—— AQS(AbstractQueuedSynchronizer)
概念
- 抽象队列同步器;volatile + cas 机制实现的锁模板,保证了代码的同步性和可见性,而 AQS 封装了线程阻塞等待挂起,解锁唤醒其他线程的逻辑。AQS 子类只需要根据状态变量,判断是否可获取锁,是否释放锁,使用 LockSupport 挂起、唤醒线程即可
- 定义:是用来实现锁或者其他同步器组件的公共基础部分的抽象实现,是重量级基础框架及整个 JUC 体系的基石,主要用于解决锁分配的问题
- 官网:为实现阻塞锁和相关的同步器提供一个框架,依赖于先进先出的一个等待;依靠单个原子 int 值来表示状态,通过占用和释放方法,改变状态值
- 整体就是一个抽象的 FIFO 队列来完成资源获取线程的排队工作,并通过一个 volatile 的 int 类型变量(state)表示持有锁的状态
- CLH(Craig、Landin、Hagersten)队列,是一个单向链表,AQS 中的队列是 CLH 变体的虚拟双向队列 FIFO
为什么 AQS 是 JUC 中最重要的基石?
- ReentrantLock 、CountDownLatch、ReentrantReadWriteLock、Semaphore 等都与 AQS 有关
- 锁 和 同步器的关系
- 锁是面向锁的使用者,定义了程序员和锁交互的使用层 API,隐藏了实现细节
- 同步器是面向锁的实现,DougLee 提出统一规范并简化了锁的实现,将其抽象出来,屏蔽了同步状态管理、同步队列的管理和维护、阻塞线程排队和通知、唤醒机制等,是一切锁和同步组件实现的公共基础部分
作用
- 多个线程抢占共享资源,只有一个线程抢占成功,其他线程必然涉及一种排队等候机制
- 如果共享资源被占用,就需要一定的阻塞等待唤醒机制来保证锁分配。这个机制主要用的是 CLH 队列的变体实现,将暂时获取不到锁的线程加入到队列中,这个队列就是 AQS 同步队列的抽象表现。它将要请求共享资源的线程及自身的等待状态封装成队列的节点对象(Node),通过 CAS、自旋以及 LockSupport.park() 的方式,维护 state 变量的状态,使并发达到同步的效果
ReentrantLock
- 公平锁 和 非公平锁的 lock() 方法唯一的区别在获取同步状态时多了一个限制条件
hasQueuedPredecessors()
,这是公平锁加锁时判断等待队列中是否存在有效节点的方法
源码分析
- 整个 ReentrantLock 的加锁过程,可以分为三个阶段
- 尝试加锁:
tryAcquire()
- 当出现 锁已经被其他线程获取 / 锁没有被其他线程获取,但当前线程需要排队 / cas 失败 这三种情况时,会导致获取锁失败
- 锁为自由状态,并不能说明可以立刻执行 cas 获取锁,因为可能在当前线程获取锁之前,已经有其他线程在排队了,必须 遵循先来后到的原则获取锁。还要调用
hasQueuedPredecessors()
方法,查看自己是否需要排队
-
加锁失败,线程进入队列:
acquireQueued()
-
线程入队列后,进入阻塞状态:
addWaiter()
- 尝试加锁:
- 具体细节查看源码
—— ReentrantReadWriteLock(可重入读写锁)
概念
- 读写锁定义:一个资源能够被多个读线程访问,或者被一个写线程访问,但是不能同时存在读写线程(读写互斥、读读共享);只有在读多写少的情景之下,读写锁才具有较高的性能表现
- 缺点:
- 写锁饥饿问题:出现大面积读锁,只有几个写锁,存在写锁长时间抢占不到锁的情况
- 锁降级:将写锁降级为读锁;锁的严苛程度变强叫升级,反之叫降级
- 如果同一个线程持有了写锁,在没有释放写锁的情况下,它还可以继续获得读锁。这就是写锁的降级,降级成了读锁
- 遵循 获取写锁——》获取读锁——》释放写锁——》释放读锁 的次序,可以实现锁降级;反之,读锁升级到写锁是不可能的
- 如果有线程在读,未释放读锁前,则该线程是无法获取写锁的,是悲观锁的策略
- 锁降级源码总结:
—— StampedLock(邮戳锁)
概念
- StampedLock 是 JDK1.8 中新增的一个读写锁,也是对 JDK1.5 中的读写锁 ReentrantReadWriteLock的优化
- stamp(戳记,long类型):代表了锁的状态。当stamp返回 0 时,表示线程获取锁失败。并且,当释放锁或者转换锁的时候,都要传入最初获取的stamp值
- 使用乐观锁策略,可以解决 ReentrantReadWriteLock 锁饥饿的问题(使用公平锁策略一定程度上也可以缓解,但是需要以牺牲系统吞吐量为代价)
- StampedLock 对短的只读代码段,使用乐观模式通常可以减少争用并提高吞吐量
缺点
- StampedLock 不支持重入,没有 Re 开头
- StampedLock 的悲观读锁和写锁都不支持条件变量(Condition)
- 使用 StampedLock 一定不要中断操作,即不要调用 interrupt() 方法
正常情况下,ReentrantLock 和 ReentrantReadWriteLock 业务场景还是较多