--- 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)