欢迎大家关注公众号——秃头让我们变强
AQS简介
编程不识Doug Lea,写尽Java也枉然。JUC包的作者,大名鼎鼎的Doug Lea,在JUC包中为我们留下了一个AbstranctQueuedSynchronizer类。希望它能满足一切并发编程开发者的同步需求。JUC包中的锁也基本都是基于AQS来实现的。AQS提供了一个volatile类型变量作为获取资源成功的标志,内部维护着一个FIFO同步队列,将获取锁失败的线程构造成一个Node节点加入同步队列中去,Node为AQS的一个内部类。并且提供了共享和独享模式的获取锁。
独享模式获取
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
tryAcquire(arg)方法在AQS中并没有实现,需要开发者自己去实现,成功返回true,失败返回false。然后调用addWaiter()方法将当前线程构造成一个独享模式的节点。
private Node addWaiter(Node mode) {
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(node);
return node;
}
可以看到,如果当前队列的尾节点不为Null,则CAS快速设置尾节点。若为Null则调用enq(node)方法设置尾节点
private Node enq(final Node node) {
for (;;) {
// 获取当前尾节点
Node t = tail;
if (t == null) {
// 说明队列是空的,初始化一下。设置一个空的头节点
if (compareAndSetHead(new Node()))
tail = head;
} else {
// 把当前节点的前驱指向尾节点
node.prev = t;
//CAS设置当前节点为尾节点
if (compareAndSetTail(t, node)) {
//将旧的尾节点的后继指针指向当前节点
t.next = node;
return t;
}
}
}
}
接着,当addWaiter()成功返回当前节点后,调用acquireQueued(Node node)方法,将当前节点加入同步队列。
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)) {
//把当前节点设置为头节点
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
//shouldParkAfterFailedAcquire判断当前节点是否需要阻塞
//parkAndCheckInterrupt阻塞当前节点
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
分析一下shouldParkAfterFailedAcquire()方法,在此之前,我们先看一下AQS内部类,即节点类
static final class Node {
static final Node SHARED = new Node();
static final Node EXCLUSIVE = null;
static final int CANCELLED = 1; //表示节点中的线程需要取消,如线程调用了中断
static final int SIGNAL = -1; //正常状态
static final int CONDITION = -2; //表示当前节点在等待队列中
static final int PROPAGATE = -3; //节点的共享状态将被传播下去
volatile int waitStatus;
//前驱节点
volatile Node prev;
//后继节点
volatile Node next;
volatile Thread thread;
Node nextWaiter;
//省略其他代码
}
知道了节点的状态,我们来看一下shouldParkAfterFailedAcquire方法
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
//获取前驱节点的状态
int ws = pred.waitStatus;
//如果节点状态为SIGNAL,即正常
if (ws == Node.SIGNAL)
//表示当前线程需要阻塞
return true;
//即CANCELLED,表示前驱节点的线程中断了,节点需要从同步队列取消
if (ws > 0) {
do {
//从后往前找,直到找到第一个节点状态非CANCELLED的
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
//将找到的节点的后继指针指向当前节点
pred.next = node;
} else {
//将前驱节点的状态设置为SIGNAL
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
如果shouldParkAfterFailedAcquire方法返回true,则代表当前线程需要阻塞,若返回false,也会在下一次循环中返回true。下面看一下parkAndCheckInterrupt()方法是如何让线程阻塞的。
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);
return Thread.interrupted();
}
可以看到,很简单,就是调用LockSuppor.park方法阻塞。当线程被唤醒时,会返回当前线程的中断标志位状态。
独享式获取总结
现在我们来总结一下流程:
-
调用tryacquire()方法尝试获取锁,若获取失败则执行步骤2
-
调用addWaiter()方法,将当前线程构造成一个节点,如果当前队列不为空,则把当前节点设置为尾节点,若为空,则先初始队列,然后将当前节点设置为尾节点,继续执行步骤3
-
调用acquireQueued()方法,判断当前节点的前驱节点是否是头节点,如果是则尝试获取锁,若获取成功,则把当前节点设为头节点并返回成功。如果前驱节点不是头节点或获取锁失败,则执行步骤4
-
调用shouldParkAfterFailedAcquire()方法,判断当前线程是否需要阻塞,如果需要,则调用parkAndCheckInterrupt()方法阻塞当前线程。当线程被唤醒时需要检查线程的中断标志位。
-
以上步骤3和步骤4是自旋的。
独享式释放锁
这里tryRelease(arg)方法同样是需要子类去实现,失败返回false,成功返回true。
public final boolean release(int arg) {
//尝试释放锁
if (tryRelease(arg)) {
Node h = head;
//释放成功,唤醒后继节点
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
看一下unparkSuccessor(node)方法,是如何唤醒后继节点的
private void unparkSuccessor(Node node) {
//获取当前节点的状态
int ws = node.waitStatus;
//如果非CANCELLED
if (ws < 0)
//设置状态为初始状态
compareAndSetWaitStatus(node, ws, 0);
//获取后继节点
Node s = node.next;
//如果后继节点为null或是CANCELLED状态
if (s == null || s.waitStatus > 0) {
//节点需要取消,helpGC
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);
}
独享式释放总结
其实独享式释放还是很简单的。
-
调用tryRelease(arg)方法尝试释放锁,释放失败返回false,释放成功进入步骤2
-
获取头节点,如果头节点不为Null并且不是初始的节点,则执行步骤3。否则直接返回true,释放成功。
-
调用unparkSuccessor(node)方法,先检查当前节点的状态,如果非CANCELLED状态,则CAS设置当前节点状态为0(即初始状态)。获取当前节点的后继节点,若后继节点是CANCELLED状态,把后继节点的引用置Null,帮助GC回收。接着从后往前遍历队列,找到第一个状态非CANCELLED的节点。如果找到了则执行步骤4
-
调用LockSupport.unpark()唤醒节点
共享式获取
先看一下acquireShared()方法
public final void acquireShared(int arg) {
if (tryAcquireShared(arg) < 0)
doAcquireShared(arg);
}
同样的,tryAcquireShared()方法没有具体的实现,需要子类自己去实现。
protected int tryAcquireShared(int arg) {
throw new UnsupportedOperationException();
}
接着我们看看当获取同步状态失败后,执行的doAcquireShared()方法。
private void doAcquireShared(int arg) {
//addWaiter()方法同之前独享模式一样,将当前线程构造成一个节点
// 加入到同步队列的尾节点,不同但是,节点的模式是SHARED,共享
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) {
// 设置当前节点为头节点,稍后详细分析此方法
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);
}
}
从前面的分析,我们可以看到,共享模式的同步状态获取和独享差不多,主要区别就在获取成功后设置头节点不同。那么我们来分析一下setHeadAndPropagate()方法。
private void setHeadAndPropagate(Node node, int propagate) {
// 获取当前头节点,即旧的头节点
Node h = head; // Record old head for check below
// 设置当前节点为头节点
setHead(node);
// 注释1:这个if条件判断很有趣,后面分析
if (propagate > 0 || h == null || h.waitStatus < 0 ||
(h = head) == null || h.waitStatus < 0) {
// 获取当前节点的后继节点
Node s = node.next;
// 后继节点为null或是共享模式
if (s == null || s.isShared())
// 唤醒节点
doReleaseShared();
}
}
看到这里你或许会疑问,前面我注释1的那个if条件判断到底该如何理解呢?
-
“propagate > 0”,代表还有同步资源可以获取,那当然要唤醒后继节点咯。
-
h==null,共享模式下,同步资源的获取和释放都是并发的,所以在当前节点获取同步资源成功,执行到if判断前,旧的头节点变为Null,那当然是释放了资源,所以要继续唤醒后面的节点
-
(h=head)==null,这里可能会有疑问,当前节点刚获取到同步资源都还未执行完方法怎么会为null呢,其实这里h=head,head不一定是当前节点。为什么呢?之前说了这个方法是并发的,那么在当前节点执行完setHead设置成head后,还未执行到if判断,此时又有新的节点获取到同步资源,然后setHead,此时head就不是当前节点了,那么这就跟问题2一样了。
-
h.waitStatus < 0 和 head.waitStatus < 0,其实是为了提高吞吐率,当一个节点成功获取到同步资源时,后面的节点也极有可能获取到资源。所以为了保守起见,waitStatus < 0时也尝试唤醒后继节点。这一点在源码注释中,Doug lea大神也说了。
我们继续分析doReleaseShared方法
private void doReleaseShared() {
for (;;) {
// 获取头节点,注意这里的头节点就是当前获取同步状态成功的节点
Node h = head;
// 头节点不是null且不等于尾节点,即当前获取同步状态成功
// 的节点还有后继节点需要唤醒
if (h != null && h != tail) {
// 获取头节点的状态
int ws = h.waitStatus;
if (ws == Node.SIGNAL) {
// CAS设置头节点的状态为0,直到成功为止。因为这个方法
// 在获取和释放都调用到了,是并发的。
if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
continue;
// 唤醒后继节点,稍后分析此方法
unparkSuccessor(h);
}
// 如果后继节点不需要唤醒,则把当前节点设置为PROPAGATE,确保以后可以传播下去
else if (ws == 0 &&
!compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
continue;
}
//如果头节点没变化,说明没有其他线程干预,如果有,则需要重新设置
if (h == head)
break;
}
}
接着分析unparkSuccessor方法是如何唤醒节点的
private void unparkSuccessor(Node node) {
// 当前节点状态
int ws = node.waitStatus;
// 非CANCELLED
if (ws < 0)
// CAS修改当前节点状态为0
compareAndSetWaitStatus(node, ws, 0);
// 当前节点的后继节点
Node s = node.next;
// 后继节点为Null或CANCELLED
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);
}
分析完共享模式的获取方法,再来看释放方法就会很简单
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
// 此方法在上面分析获取时,已经分析
doReleaseShared();
return true;
}
return false;
}
同样的,tryReleaseShared()方法需要我们自己去实现,成功返回true,失败返回false。而doReleaseShared()方法,跟前面获取调用的是同一个方法,所以不再叙述。这也应征了前面所说的,共享模式的获取和释放是并发的。
结语
至此,我们通过两篇文章学习了AQS共享模式和独占模式下的同步资源获取/释放。对AQS也有了较深的理解。可以看到AQS是相当重要的,也深深体会到Doug lea大神的超人智慧。希望有朝一日,我们在座所有的各位都能有这样精妙的设计。Thanks。
更多请关注公众号——秃头让我们变强