AQS-核心概念
理解 AQS 的核心问题
AQS 是一个用于构建锁、同步器等线程协作工具类的框架,有了 AQS 以后,很多用于线程协作的工具类就都可以很方便的被写出来,可以极大地减少更上层的开发工作量,避免重复造轮子,同时也避免了上层因处理不当而导致的线程安全问题。基于 AQS 的实现,JDK 已经为我们提供了多个实现线程协作工具类,此外我们可以在理解 AQS 的基础上实现自己业务的线程协作器。
带着问题理解 AQS:
- 线程如何获取锁,都做什么处理;
- 线程获取不到锁,应该做什么处理;
- 获取到锁的线程释放锁后,其他线程做什么处理;
理解这几个问题,对 AQS 的理解更加立体。
AQS 的核心概念
state/等待队列/条件队列
AQS 中主要维护了 state(锁状态的表示)、等待队列(CHL)、条件队列。
- state 是临界资源,也是锁的描述。表示有多少线程获取了锁;
- 等待队列维护了获取不到锁的线程(Node),队列中的元素 Node 代表线程的信息;
- 条件队列维护了获取等待条件唤醒的线程(Node);
其中 state 维护了线程获取锁的次数,当线程获取锁一次 state 加一,同一个线程获取 N 次,那么 state = N。
/**
* The synchronization state.
*/
private volatile int state;
/**
* Returns the current value of synchronization state.
* This operation has memory semantics of a {@code volatile} read.
* @return current state value
*/
protected final int getState() {
return state;
}
/**
* Sets the value of synchronization state.
* This operation has memory semantics of a {@code volatile} write.
* @param newState the new state value
*/
protected final void setState(int newState) {
state = newState;
}
等待队列(也叫 CHL 队列,同步队列)使用了双向链表实现,将获取不到锁的线程抽象为一个节点(Node 内部类),维护到链表中,使用 head 节点、tail 节点、next 指向、prev 指向进行维护。当有线程获取锁失败,就被添加到队列末尾。
private transient volatile Node head;
private transient volatile Node tail;
如果锁增加条件等待和唤醒,那么 AQS 同时维护了条件队列,该队列的实现和等待队列相同,也是使用相同Node内部类,维护到链表中。如果没有对该锁调用启用条件等待的 API 时,该队列不存在。
Condition condition = lock.newCondition();
condition.await();
线程节点和队列
线程节点中主要维护了 模式、waitStatus、thread 和 链表信息:
- SHARED、EXCLUSIVE:表示节点时的模式(共享、独占);
- waitStatus:等待状态,CANCELLED 、SIGNAL、CONDITION、PROPAGATE、默认是 0;
- thread:线程,对于等待的线程,同步器将获取尝试获取锁不成功的线程和节点关联,可以理解节点 = 线程;
- 链表信息:prev、next 链表的指向指针,用于链表查找、插入和删除操作。
class Node {
/**
* 用于表示节点是共享模式下的等待节点
*/
static final Node SHARED = new Node();
/**
* 用于表示节点是独占模式下的等待节点
*/
static final Node EXCLUSIVE = null;
/**
* 线程被取消了
*/
static final int CANCELLED = 1;
/**
* 释放资源后需唤醒后继节点
*/
static final int SIGNAL = -1;
/**
* 等待condition唤醒
*/
static final int CONDITION = -2;
/**
* 工作于共享锁状态,需要向后传播,比如根据资源是否剩余,唤醒后继节点
*/
static final int PROPAGATE = -3;
/**
* waitStatus 用于标识线程的状态,取值有:SIGNAL、CANCELLED、CONDITION、PROPAGATE
* 这些值按数字排列以简化使用,>=0 标识线程不需要被通知唤醒,大多数时候程序不需要
* 检验程序的确定值,只需要看是不是 >=0 或者 <0; 就可以知道程序是否被唤醒,对于普
* 通同步节点,该字段被初始化为 0,并且对于条件节点,该字段被初始化为 CONDITION,
* 使用CAS进行修改。
*/
volatile int waitStatus;
/**
* 当前节点的前驱节点
*/
volatile Node prev;
/**
* 当前节点的后驱节点
*/
volatile Node next;
/**
* 当前节点对应的线程
*/
volatile Thread thread;
/**
* 下一个正在进行条件等待节点(线程)
*/
Node nextWaiter;
/**
* 判断下一个等待线程是否是共享模式
*/
final boolean isShared() {
return nextWaiter == SHARED;
}
final Node predecessor() throws NullPointerException {
Node p = prev;
if (p == null)
throw new NullPointerException();
else
return p;
}
/**
* 空构造
*/
Node() { // Used to establish initial head or SHARED marker
}
/**
* 构造不同模式的节点
*/
Node(Thread thread, Node mode) { // Used by addWaiter
this.nextWaiter = mode;
this.thread = thread;
}
/**
* 构造不同状态的节点
*/
Node(Thread thread, int waitStatus) { // Used by Condition
this.waitStatus = waitStatus;
this.thread = thread;
}
}
AQS 将获取锁的线程抽象为 Node, 获取不到锁的线程 AQS 会生成一个节点,将其插入到队列的尾部,待持锁的线程释放锁之后,再重新唤起队列中的线程。因此复杂的线程同步器转化为队列的操作。当然 AQS 显然不这么简单,但这不失为对 AQS 初步的一个理解和印象。
addWaiter 函数是源码中重要的函数,从代码来看,使用当前线程(尝试获取锁的线程)和模式(独占/共享)两个参数实例化出一个节点,判断链表(队列)是否为空,不空就将新节点插入到链表的尾部,如果为空就申请队列。抛开线程的相关知识,这是链表的最为实际的例子。理解链表的操作,便可以理解 AQS 实现大部分逻辑。
private Node addWaiter(Node mode) {
Node node = new Node(Thread.currentThread(), mode);
Node pred = tail;
if (pred != null) {
node.prev = pred;
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
enq(node);
return node;
}
private Node enq(final Node node) {
for (;;) {
Node t = tail;
if (t == null) { // Must initialize
if (compareAndSetHead(new Node()))
tail = head;
} else {
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
从整体上来讲,AQS 底层是使用双向链表实现的队列,将线程和和线程状态封装到一个节点中,在线程获取不到锁资源时候将线程加入到队列中,调用特定 API (LockSupport.park)将线程阻塞住,等待获取到锁资源的线程释放锁资源之后,让队列中的线程节点出队并且调用特定 API (LockSupport.unpark)唤醒,从而获取锁资源。