概述
今天聊下并发编程中很重要的一个类AbstractQueuedSynchronizer,简称AQS,为什么说他重要?并发库作者Doug Lea设计之初就是期望它能够成为实现大部分同步需求的基础。因此看懂了它,将很容易学习(ReentrantLock、ReentrantReadWriteLock、CountDownLatch、Semaphore等),而且使用它可以很容易搭建自定义的同步器。
AQS使用的是典型的模版方法模式,子类自定义同步器时只需要实现少数的几个方法就行,屏蔽了大量的细节,例如获取同步状态、FIFO同步队列。基于AQS不仅能极大的降低开发工作量,而且降低了并发竞争位置时出错的概率。
AQS提供独占锁和共享锁两种获取锁的方式。
独占式exclusive:保证一次只有一个线程可以获取到锁。
共享式shared:允许多个线程同时获取到锁。
public abstract class AbstractQueuedSynchronizer implements java.io.Serializable {
protected AbstractQueuedSynchronizer() {}
//同步队列中的节点
static final class Node {}
//队列的头节点
private transient volatile Node head;
//队列的尾节点
private transient volatile Node tail;
//同步状态: state=0时表示无线程锁定, state>0时表示有线程锁定中
private volatile int state;
...
}
可以看到AQS中维护了一个队列和一个同步状态state。
队列: 是一个CHL队列(FIFO),用来存储等待获取锁的线程。
state: 是同步的状态,等于0表示未锁定,大于0表示已有线程锁定。
队列结构图如下:
接下来我们看下节点Node类的内部:
static final class Node {
//等待状态:有SIGNAL,CANCELLED,CONDITION,PROPAGATE,0
volatile int waitStatus;
//当前节点的前节点
volatile Node prev;
//当前节点的后节点
volatile Node next;
//节点的线程: 毕竟竞争的主体是线程
volatile Thread thread;
//存储condition队列中的后继节点。
Node nextWaiter;
...
}
通过prev、next可以看到节点是双向引用的,我们重点关心等待状态waitState,waitState取值的时机一定要清楚。
- 0: 新节点入队是的默认值。
- CANCELLED (1): 表示当前结点已取消调度。当timeout或被中断(响应中断的情况下),会触发变更为此状态,进入该状态后的结点将不会再变化。
- SIGNAL (-1):表示后继结点在等待当前结点唤醒。后继结点入队时,会将前继结点的状态更新为SIGNAL。
- CONDITION(-2):表示结点等待在Condition上,当其他线程调用了Condition的signal()方法后,CONDITION状态的结点将从等待队列转移到同步队列中,等待获取同步锁。
- PROPAGATE(-3):共享模式下,前继结点不仅会唤醒其后继结点,同时也可能会唤醒后继的后继结点。
注意: 负值表示结点处于有效等待状态,而正值表示结点已被取消。源码中很多地方用>0、<0来判断结点的状态是否正常。
下面我们一起看一下AQS最重要的几个方法:acquire(独占式获取锁)、release(独占式释放锁)、acquireShared(共享式获取锁)、releaseShared(共享式释放锁)
acquire 独占式获取锁
public final void acquire(int arg) {
//尝试获取锁,获取成功则返回,否则加入等待队列
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
- tryAcquire尝试获得锁,因为是短路与(&&),tryAcquire成功,!tryAcquire()为false,则跳出if,不在进行后续操作。
- 如果获取失败则进入addWaiter方法,构造同步节点(独占式Node.EXCLUSIVE),将该节点添加到同步队列尾部,并返回此节点,进入acquireQueued方法。
- acquireQueued中如果前继节点是头节点,再次尝试换取锁,失败则将正常前继节点的waitState设置为SIGNAL,然后阻塞自己,那么该线程被唤醒就只可能是前继节点发起唤醒,或者线程被中断。该方法有一点歧义,返回值其实是中断状态,很容易让人以为是是否请求队列成功,这个后面会详细说明。
- 所以当if条件为真,就是没有请求成功,且线程被中断了,则再次执行中断方法selfInterrupt。
tryAcquire失败后进入addWaiter方法
private Node addWaiter(Node mode) {
//用当前线程和以给定模式构造结点。mode有两种:EXCLUSIVE(独占)和SHARED(共享)
Node node = new Node(Thread.currentThread(), mode);
// 尝试插入尾节点的后面成为新的尾节点
Node pred = tail;
if (pred != null) {
node.prev = pred;
//CAS操作将新节点设为尾节点
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
//前继节点为空或者并发原因设置尾节点失败,进入enq方法
enq(node);
return node;
}
上面方法主要逻辑已注释,这里简单介绍下CAS操作
CAS需要有3个操作数:内存地址V,旧的预期值A,即将要更新的目标值B。
CAS指令执行时,当且仅当内存地址V的值与预期值A相等时,将内存地址V的值修改为B,返回成功,否则就什么都不做,返回失败。整个比较并替换的操作是一个原子操作。
enq方法
private Node enq(final Node node) {
//死循环和compareAndSetTail方法,以CAS"自旋"方式,直到成功加入队尾
for (;;) {
Node t = tail;
if (t == null) {
//队列为空则创建一个空节点作为头节点,这里因为并发原因使用CAS操作
if (compareAndSetHead(new Node()))
tail = head;
} else {
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
到了这里,等待线程已经成功入队了。下面进入acquireQueued方法
acquireQueued方法
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
//得到前继节点
final Node p = node.predecessor();
//前继节点是头节点,可能头节点已经释放了,所以再次尝试加锁
if (p == head && tryAcquire(arg)) {
//成功后将节点设为头节点,这时没有并发,所以不用CAS操作了
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
//如果p节点不是头节点,或者tryAcquire返回false,说明请求失败。
//那么先判断请求失败后node节点是否应该被阻塞,
//如果应该被阻塞,则阻塞node节点,当被唤醒后检测中断状态。
//如果if为true说明,是中断的唤醒,将interrupted设为true
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
- 如果当前节点是第二个节点,再次尝试加锁,成功后将节点设为头节点
- 当前节点不是第二节点,或者请求加锁失败,判断是否应该被阻塞,其实就是让有效前继节点的waitState为Signal
- 设置成功以后中断本节点的线程
- 方法返回的其实是interrupted的状态,调用程序可以自行决定是否处理中断
- finally中判断了failed状态,for循环可以看到,如果正常返回failed肯定是false,为 true说明发生异常,进行取消请求操作
shouldParkAfterFailedAcquire方法
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus;
if (ws == Node.SIGNAL)
//前继节点waitStatus为SIGNAL,就可以放心的阻塞了
return true;
if (ws > 0) {
//跳过waitStatus为CANCLE的节点,这些都是无效前继节点了
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
//前继节点waitStatus是0或者PROPAGATE
//将前继节点的状态设为false 方法返回false,进入下一次循环
//直到前继节点waitStatus为SIGNAL,然后就可以去阻塞线程了
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
当然shouldParkAfterFailedAcquire方法返回true以后,进入parkAndCheckInterrupt方法
parkAndCheckInterrupt
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);//阻塞自己
return Thread.interrupted();//当线程被唤醒后,代码从这里开始,返回中断状态
}
- 阻塞自己
- 返回中断状态,因为唤醒有两种方式:一种unpark, 一种线程interrupt
以上就是独占式获取锁方法的全部代码了,接下来看看如何释放
release独占式释放锁
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
- 尝试释放独占式锁
- 成功以后将头节点释放,唤醒头节点的后继节点
private void unparkSuccessor(Node node) {
//如果waitStatus的值小于0,CAS设为0
int ws = node.waitStatus;
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
//如果后继节点不为空直接进行unpark
//否则从尾部向前找不是取消状态的实际后继节点
Node s = node.next;
if (s == null || s.waitStatus > 0) {
s = null;
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)
s = t;
}
if (s != null)
LockSupport.unpark(s.thread);
}
释放独占锁成功,我们再回头看一下当头节点释放后,头节点的后继节点线程被unpark唤醒,parkAndCheckInterrupt方法会返回,acquireQueued方法内进入下一次循环,这次循环前继节点就是Head了,整个加锁解锁就形成闭环了。
acquireShared 共享式获取锁
public final void acquireShared(int arg) {
if (tryAcquireShared(arg) < 0)
doAcquireShared(arg);
}
- 尝试获取共享锁,当返回值大于等于0时,说明加锁成功
- 如果失败,执行doAcquireShared,加入等待队列
private void doAcquireShared(int arg) {
//构造共享式等待节点,和上面分析一致
final Node node = addWaiter(Node.SHARED);
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor();
if (p == head) { //如果前继节点时头节点,尝试获取锁
int r = tryAcquireShared(arg);
if (r >= 0) {//当返回值大于等于0时,说明加锁成功
setHeadAndPropagate(node, r); //设置头节点并传播
p.next = null; // help GC
if (interrupted)
selfInterrupt();
failed = false;
return;
}
}
//这两个和之前的独占式逻辑一致
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
//发生异常取消获取锁的请求
if (failed)
cancelAcquire(node);
}
}
逻辑和acquireQueued类似
我们重点分析加锁成功后的setHeadAndPropagate方法
setHeadAndPropagate设置头节点并传播
private void setHeadAndPropagate(Node node, int propagate) {
Node h = head; // Record old head for check below
setHead(node); //将节点设置为头节点
//如果propagate > 0说明还有资源,或者后继节点正在等待时
//进行传播
if (propagate > 0 || h == null || h.waitStatus < 0 ||
(h = head) == null || h.waitStatus < 0) {
Node s = node.next;
//下一节点在共享模式下等待时,进行唤醒
if (s == null || s.isShared())
doReleaseShared();
}
}
上面可以看到,多个if条件可能导致不必要的唤醒,但Doug Lea认为这些唤醒也是即将要发生的,可以忍受。
另外这个方法调用了doReleaseShared,就是唤醒后续等待节点。
releaseShared共享式释放锁
private void doReleaseShared() {
for (;;) {
Node h = head;
if (h != null && h != tail) {
// 获取头节点对应的线程的状态
int ws = h.waitStatus;
// 如果头节点对应的线程是SIGNAL状态
//则意味着“头节点的下一个节点所对应的线程”需要被unpark唤醒。
if (ws == Node.SIGNAL) {
// 因为存在并发,所以CAS设置头节点状态为0,成功则进行唤醒后续节点
//如果失败继续执行for循环。
if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
continue;
// 唤醒“头节点的下一个节点所对应的线程”。
unparkSuccessor(h);
}
// 如果头节点对应的线程是空状态,将状态设置为PROPAGATE
else if (ws == 0 &&
!compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
continue; // loop on failed CAS
}
// 如果头节点发生变化,则继续循环。否则,退出循环。
if (h == head) // loop if head changed
break;
}
}
可以看到共享锁唤醒后继节点比独占锁复杂,因为共享锁是并发操作,所以使用循环和CAS保证并发的安全性。