AQS(AbstractQueuedSynchronizer)
又称队列同步器,是 JUC
包里锁组件、同步工具的基础框架。它使用一个 int
成员变量表示同步状态,通过内置的 FIFO
来完成资源获取线程的排队工作。
设计思想
同步器背后的基本思想非常简单。acquire操作如下:
while (synchronization state does not allow acquire) {
enqueue current thread if not already queued;
possibly block current thread;
}
dequeue current thread if it was queued;
release操作如下:
update synchronization state;
if (state may permit a blocked thread to acquire)
unblock one or more queued threads;
根据上述的流程我们可以类比 synchronized
的流程:
点击查看【processon】
当线程成功获取 Monitor
时,会去执行 synchronized
代码块;如果线程获取 Monitor
失败了,就会进入一个叫 SynchronizedQueeu
的队列中等待(即线程状态 Blocked
),当 Montior
的持有者释放 Monitor
时,才会去通知队列里的线程出队。
其实同步器和 synchronized
的流程十分相似,为了实现上述的操作(不考虑细节),我们可以粗略地提出三个核心元素:
- 线程获取
Monitor
成功/失败怎么保存?是否还会有其他状态? ——同步状态的原子性管理 - 线程进入队列后应该如何阻塞? ——线程的阻塞与解除阻塞
- 保存线程的队列? ——等待队列的管理
核心三要素
根据前面的理解,我们看看 Douge Lea
大神是如何实现这三个核心要素的。
同步状态的原子性管理
AQS
同步器里,使用32位 int
来表示同步状态,并暴露出对应的更新方法,例如 setState()
、 getState()
、 compareAndSetState()
等方法。这些方法均作用于 volatile
变量,故能保持变量的可见性以及读写的原子性。
线程的阻塞与解阻塞
由于锁会被多个线程争来争去,最终只会有一个线程拿到锁,其余的线程等待。那么要如何实现等待?
在 JDK1.6
之前,除 synchronized
外没有其他的阻塞手段了( suspend()
、 resume()
这俩方法有问题被废弃了,废弃原因参照这个),因此重新设计了一个类 LockSuport
来解决该问题。 LockSupport
提供了 park()
和 unpark()
的方法:
park(Object)
:阻塞当前线程unpark(Thread)
:使当前线程退出阻塞
其底层是使用的 Unsafe
这个类,详细内容可以看这里。
等待队列管理
AQS
的核心数据结构就是线程等待队列,AQS
的线程等待队列是 CLH(Craig, Landin, anHagersten)
自旋锁队列的一种变种,两者的主要差距体现在—— AQS
的线程等待队列是阻塞的而 CLH
是自旋的,其余的内容基本一样。详细的内容可以参照《Java并发 - CLH锁、MCS锁》 。
Doug Lea选择 CLH 队列的原因:同步队列的最佳选择是自身没有使用底层锁来构造的非阻塞数据结构,目前,业界对此很少有争议。而其中主要有两个选择:一个是Mellor-Crummey和Scott锁(MCS锁)的变体,另一个是Craig,Landin和Hagersten锁(CLH锁)的变体。一直以来,CLH锁仅被用于自旋锁。但是,在这个框架中,CLH锁显然比MCS锁更合适。因为CLH锁可以更容易地去实现“取消(cancellation)”和“超时”功能,因此我们选择了CLH锁作为实现的基础。 |
|
---|---|
线程等待队列本质就是通过链表实现的队列,只不过这个线程等待队列的元素是线程罢了,而且这些线程必须处于阻塞状态。
整体架构
此图来自于《从ReentrantLock的实现看AQS的原理及应用》,将 AQS
拆成了各个层次,个人觉得思路挺清晰的~
FROM 《从ReentrantLock的实现看AQS的原理及应用》
有颜色的为方法,无色的为属性,各层的主要关系如下所示:
API
层:将属性暴露给开发者,便于开发者创建自己的同步组件- 锁获取方法层:
API层
依赖此层处理锁相关的事情 - 队列方法层、排队方法层:对于“锁获取方法层”竞争失败的线程,会调用该层的方法进入等待队列
- 数据提供层:上面的所有层次均依赖“数据提供层”的状态,从而决定下一步的执行
组件源码剖析
核心组件——同步状态
AQS
会维护着一个 volatile
的同步状态:
private volatile int state;
核心组件——LockSupport
一个可以创建锁、同步器的基础线程阻塞的底层工具。
- 调用
LockSupport.park()
,将在调用线程中消耗permit
。如果permit
可用,那么就会立即返回;否则阻塞调用线程 - 调用
LockSupport.unpark()
,将会使得permit
可用(LockSupport.unpark()
至多只有一个permit
)
LockSpport的优势
针对线程的阻塞和解除阻塞,我们很容易想到 Thread.suspend()
和 Thread.resume()
两个方法,然而这两个方法因为天生容易发生死锁(suspend()方法原理是暂停JVM,而暂停JVM是不稳定不安全的,所以容易发生问题)从而被抛弃使用。
具体可以看我的《为什么Thread.suspend和resume被弃用》一文
而 Object.wait()
和 Object.notify()
使用起来过于麻烦,因为要放在 synchronized
块里,所以 LockSupport
就成为了很好的替代品。
核心结构——同步队列
AQS
里的同步队列是将 Node
节点链接起来的一个队列,核心类是 Node
:
static final class Node {
// 标记一个Node是处于 共享模式
static final Node SHARED = new Node();
// 标记一个Node是处于 排他模式
static final Node EXCLUSIVE = null;
// 状态常量,后续会分析的
...
// 保存同步状态的变量,核心要素一:同步状态的原子性管理
volatile int waitStatus;
// 前一个Node
volatile Node prev;
// 后一个Node
volatile Node next;
// 该Node代表的线程
volatile Thread thread;
// Condition模式下的后一个Node
Node nextWaiter;
// 判断该节点是否为 共享模式
final boolean isShared(){...}
// 返回前一个Node
final Node predecessor() throws NullPointerException {...}
// 初始化HEAD
Node() {}
// addWaiter() 使用
Node(Thread thread, Node mode) {
this.nextWaiter = mode;
this.thread = thread;
}
// condition模式使用
Node(Thread thread, int waitStatus) {
this.waitStatus = waitStatus;
this.thread = thread;
}
}
总结一下,等待队列有两种锁模式:
模式 | 含义 |
---|---|
SHARED | 表示线程以共享的模式等待锁 |
EXCLUSIVE | 表示线程正在以独占的方式等待锁 |
waitStatus有5种状态:
枚举 | 含义 |
---|---|
0 | 当一个Node被初始化的时候的默认值 |
CANCELLED | 为1,表示线程获取锁的请求已经取消了 |
CONDITION | 为-2,表示节点在等待队列中,节点线程等待唤醒 |
PROPAGATE | 为-3,当前线程处在SHARED情况下,该字段才会使用 |
SIGNAL | 为-1,表示线程已经准备好了,就等资源释放了 |
AQS
的等待队列本质就是一个双向链表结构的队列:
点击查看【processon】
流程源码分析
本篇文章主要分析 AQS
的源码流程,将从下面几个部分进行分析:
- 独占锁
- 获取锁
- 释放锁
- 等待队列
- 共享锁
- 获取锁
- 释放锁
- 等待队列
- Condition
由于 ReentrantLock
对 AQS
的实现比较完全,所以会通过 ReentrantLock
引入 AQS
的源码分析。