parent
5e1538d85b
commit
444fe4183a
@ -0,0 +1,145 @@ |
||||
--- |
||||
synchronized、CAS、AQS |
||||
--- |
||||
|
||||
#### 目录 |
||||
|
||||
1. 思维导图 |
||||
2. 概述 |
||||
3. 常见术语 |
||||
- 悲观锁/乐观锁 |
||||
- 公平锁/非公平锁 |
||||
- 可重入锁/不可重入锁 |
||||
- 死锁 |
||||
4. synchronized |
||||
- 使用方式 |
||||
- 实现原理 |
||||
- 锁优化 |
||||
5. CAS |
||||
6. AQS |
||||
- Lock |
||||
- ReadWriteLock |
||||
7. 参考 |
||||
|
||||
#### 思维导图 |
||||
|
||||
#### 概述 |
||||
|
||||
Java 中的并发锁大致上可以分为隐式锁和显式锁两种。隐式锁的代表就是 synchronized 关键字,显式锁主要包含两个接口:Lock 和 ReadWriteLock,主要实现类分别为 ReentrantLock 和 ReentrantReadWriteLock,这两个类都是基于 AQS(AbstractQueuedSynchronizer)队列同步器实现的,还有的地方把 CAS 也称为一种锁,在包括 AQS 在内的很多并发相关类中,CAS 都扮演了很重要的角色,比如 AtomicInteger 等原子操作类。 |
||||
|
||||
#### 常见术语 |
||||
|
||||
##### 悲观锁和乐观锁 |
||||
|
||||
悲观锁和独占锁是一个意思,它假设一定会发生冲突,因此获取到锁之后会阻塞其他等待线程。这么做的好处是简单安全,但是挂起线程和恢复线程都需要转入内核态进行,这样做会带来很大的性能开销。悲观锁的代表是 synchronized,然鹅在真实环境中,大部分时候都不会产生冲突。而乐观锁不一样,它假设不会产生冲突,先去尝试执行某项操作,失败了再进行其他处理(一般都是不断循环重试),这种锁不会阻塞其他线程,也不涉及上下文切换,性能开销小,代表实现是 CAS。 |
||||
|
||||
##### 公平锁和非公平锁 |
||||
|
||||
公平锁是指各个线程在获取锁前先检查有无排队的线程,按排队顺序去获取锁。非公平锁是指线程获取锁前不考虑排队问题,直接尝试获取锁。值得注意是,在 AQS 的实现中,一旦线程进入排队队列,即使是非公平锁,线程也得乖乖排队。 |
||||
|
||||
##### 可重入锁和不可重入锁 |
||||
|
||||
如果一个线程已经获取到了锁,那么它可以访问被这个锁锁住的所有代码块,不可重入锁与之相反。可重入锁包括 synchronized 和 ReentrantLock。 |
||||
|
||||
##### 死锁 |
||||
|
||||
|
||||
|
||||
### synchronized |
||||
|
||||
悲观、可重入,已经不那么重量级的锁。 |
||||
|
||||
##### 使用方式 |
||||
|
||||
- 修饰静态方法,锁 class 对象 |
||||
- 修饰非静态方法,锁当前实例对象 |
||||
- 修饰代码块,锁的是代码块里的对象 |
||||
|
||||
每个对象都有一个锁和一个等待队列,锁只能被一个线程持有,其他需要获取锁的线程需要阻塞等待,锁释放之后,对象会从队列中取出一个线程并唤醒,唤醒哪个线程是不确定的,不保证公平性。 |
||||
|
||||
多个线程是可以同时执行同一个 synchronized 实例方法的,只要它们访问的对象是不同的。 |
||||
|
||||
synchronized 锁住的是对象而非代码,只要访问的是同一个对象的方法,都得顺序访问。 |
||||
|
||||
##### 实现原理 |
||||
|
||||
synchronized 是基于 Java 对象头和 Monitor 机制来实现的。 |
||||
|
||||
- Java 对象头 |
||||
|
||||
一个对象在内存中包含三个部分:对象头、实例数据、对其填充。其中 Java 对象头包含两个部分: |
||||
|
||||
- Class Metadata Adderess 类型指针 |
||||
|
||||
存储类的元数据的指针,虚拟机通过这个指针找到它是哪个类的实例。 |
||||
|
||||
- Mark Work 标记字段 |
||||
|
||||
存储对象自身的运行时数据,包括 hashCode、GC 分代年龄、锁状态标志等。 |
||||
|
||||
- Monitor |
||||
|
||||
Mark Word 有一个字段指向 monitor 对象,monitor 中记录了锁的持有线程,等待的线程队列等信息,前面说的每个对象都有一个锁和一个等待队列,就是在这实现的。 |
||||
|
||||
那 JVM 是如何把 synchronized 与 monitor 关联起来的呢?可以分为两种情况: |
||||
|
||||
1. 同步代码块,编译时会直接在同步代码块前加上 monitorenter 指令,代码块后加上 monitorexit 指令。 |
||||
2. 同步方法,虚拟机会为方法设置 ACC_SYNCHRONIZED 标志,在调用方法的时候根据这个标志判断是否是同步方法。 |
||||
|
||||
##### 锁优化 |
||||
|
||||
JDK 1.6 对锁的实现引入了大量的优化,包括自旋锁、适应性自旋锁、锁消除、锁粗化、偏向锁、轻量级锁来减少锁操作的开销。 |
||||
|
||||
锁主要存在四种状态:依次是无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态。锁可以升级但是不能降级,这种策略是为了提高获取锁和释放锁的效率。 |
||||
|
||||
- 重量级锁 |
||||
|
||||
重量级锁就是采用互斥量 Mutex 来控制对互斥资源的访问,在 Java 中被抽象为监视器锁 Monitor,这种同步方式成本非常高,包括内核态到用户态切换,性能差。 |
||||
|
||||
- 自旋锁与自适应自旋 |
||||
|
||||
在许多应用中,锁定状态只会持续很短的时间,为了这么一点时间就去挂起、恢复线程,不值得。我们可以让等待线程执行一定次数的循环,在循环中去获取锁,这项技术称为自旋锁,它可以节省系统切换线程的消耗,但仍然占有处理器。在之后又引入自适应的自旋锁,不再通过次数来限制,而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。 |
||||
|
||||
- 锁消除 |
||||
|
||||
虚拟机在运行时,如果发现一段被锁住的代码中不可能存在共享数据,就会将这个锁清除。 |
||||
|
||||
- 锁粗化 |
||||
|
||||
当虚拟机检测到有一串零碎的操作都对同一个对象加锁时,会把锁扩展到整个操作序列外部。如 StringBuffer 的 append 操作。 |
||||
|
||||
- 轻量级锁 |
||||
|
||||
对绝大部分锁来说,整个同步周期内不存在竞争,如果没有竞争,轻量级锁可以使用 CAS 操作来避免使用互斥量的开销。 |
||||
|
||||
- 偏向锁 |
||||
|
||||
偏向锁的核心思想是,如果一个线程获得了锁,那么锁就进入偏向模式,当这个线程再次请求锁时,无需再做任何同步操作,即可获取锁。 |
||||
|
||||
#### CAS |
||||
|
||||
CAS 即 CompareAndSwap 比较并替换。CAS 机制当中使用了三个基本操作数:内存地址 V,旧的预期值 A,要修改的新值 B。更新一个变量的时候,只有当变量的预期值 A 和内存地址 V 和实际值相同时,才会将内存地址 V 对应的值修改为 B。 |
||||
|
||||
但是问题也是显而易见的: |
||||
|
||||
1. ABA 问题 |
||||
|
||||
加一个版本号。 |
||||
|
||||
2. 不能保证代码块的原子性 |
||||
|
||||
3. 在多次尝试更新一个值时不成功,CPU 开销大 |
||||
|
||||
在 Java 并发包中 AtomicInteger、等都是它的实现。 |
||||
|
||||
#### AQS |
||||
|
||||
|
||||
|
||||
#### 参考 |
||||
|
||||
[从 synchronized 到 CAS 和 AQS - 彻底弄懂 Java 各种并发锁](https://juejin.im/post/5c37377351882525ec200f9e) |
||||
|
||||
[synchronize早已经没那么笨重](https://juejin.im/post/5bff854b5188250e8601ec90) |
||||
|
||||
[什么是 CAS 机制?](https://www.jianshu.com/p/41216f83c0e1) |
Loading…
Reference in new issue