写在前面
AQS
也是在来来回回看了源码好多遍,才有所理解。原本打算就这一篇写完,点到即止,但不知觉的又深入了,无法抗拒的代码魅力啊!所以分为两篇,一篇共享式,一篇独占式,美滋滋呢。
什么是 AQS
AQS
全称 AbstractQueuedSynchronizer
,从类名可以知道,它是一个抽象类,并且可能维护了队列,最主要的作用是作为同步器。
在 JUC
包下,很多同步工具类使用了它,使用的方式并不是直接继承该类,而是使用内部类的方式;
有关同步工具类,可以参考后续的推荐博文。
这样看来,还是“半知半解”,且看下文。
如何理解 AQS
我所理解同步的本质持有锁,访问共享资源,释放锁;共享资源的访问是由调用方决定的,所以,只有在持有锁和释放锁上面做文章。这里先抛出问题,然后 AQS
来解决问题。
从面向对象的角度来看,AQS
是针对同步问题的一种抽象,它并不代表某种具体的同步工具,但将同步工具中某些共性给抽取了出来,以方便编写同步工具类。
这里的共性可以理解为一些实现细节,列如:
-
当前线程到底如何才能挂起?
LockSupport.park
方法; -
如何表示持有锁?
AQS
的 state 属性值,通过该值可以判断是否持有锁; -
如何表示释放锁
仍然是通过 state 值判断;
这些实现细节是比较复杂的,但我觉得聪明之处在于,把持有锁或者释放锁的请求,交由子类去实现,典型的模板方法模式。
共享锁的锁获取
先写下共享锁获取,如下面的:
public final void acquireSharedInterruptibly(int arg)
throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
if (tryAcquireShared(arg) < 0)
doAcquireSharedInterruptibly(arg);
}
该方法是用于共享模式下的锁获取,其中 tryAcquireShared
方法由子类实现,如果锁获取失败,线程将排队,则由 doAcquireSharedInterruptibly
方法实现,子类无需关心;子类无需关心线程如何排队,它仅仅需要关心锁是否获取成功,而对于锁的获取成功与否,是和 AQS
的 state
属性有关,针对该属性,仅仅提供了以下几种方法查看或者修改其值:
protected final int getState() {
return state;
}
protected final void setState(int newState) {
state = newState;
}
protected final boolean compareAndSetState(int expect, int update) {
// See below for intrinsics setup to support this
return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}
用流程图描述大概如下:
上面这种流程不完全准确,不准确的原因仅仅在于真实情况更加复杂,但仍然具有参考意义;比如上面的几个点:
- CLH 队列:这是需要搞懂的几个点,这个队列是链表实现的,存放的数据元素是线程,以及一个
waitStatus
,根据该值可以决定后继线程的状态,所以,会有一个哨兵节点,该节点并未持有线程,但拥有等waitStatus
值。 - park 和 unpark:节点持有了线程,那么在合适的情况下,就可以通过
LockSupport.park
和LockSupport.unpark
方法控制线程的执行了。
上面的两个知识点比较重要的,CLH
队列是一种理论的实现,还有 CAS
,这是需要底层的支持的,它解决的是原子问题,即比较值和设置值是一个原子操作,而 CAS
自旋能够在不加锁的情况下,安全地对共享变量进行写操作,它的本质:比较和交换,也就是说 a 线程准备为变量设置值时,针对变量,它会有一个预期值,比如说 a 线程认为变量此刻值是 1,那么在 CAS
执行时,如果未有线程修改过该变量值,那么 CAS
执行成功;如果中途 b 线程修改了该变量值,那么 CAS
执行失败,此时,开始重新循环,利用新的值参与运算,重复以上过程,这就是自旋的意思;CAS
自旋有一个 ABA 问题,也就是说先修改了变量为 2,又重新改为 1,这时候对于 a 线程是无法感知的,这是一种特殊情况,如果处理逻辑能够容忍这种情况,那么也是没有问题的;
共享锁的释放
共享锁的释放代码如下:
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
doReleaseShared();
return true;
}
return false;
}
这也是模板方法模式,tryReleaseShared
表示锁的释放成与否,如果锁成功被释放,那么需要唤醒队列中的其它线程,所以 doReleaseShared
方法做的就是这件事;
交互
关于释放了共享锁如何换醒其它线程,我就放在这里了,这应该才是重点吧!
两个最主要的方法:doReleaseShared
和 doAcquireSharedInterruptibly
方法;
doReleaseShared
能够唤醒头结点的后继节点, 而 doAcquireSharedInterruptibly
方法中,后继节点被唤醒后,会重新进入循环,那时候又会调用 doReleaseShared
方法,直到唤醒完所有节点,这就是共享锁的交互过程;
private void doAcquireSharedInterruptibly(int arg)
throws InterruptedException {
/**
* 创建节点,放置队列末尾,如果头节点为 null,会初始化话一个空的 node 节点,作为头节点,然后该 * 节点将作为头结点的后继节点
**/
final Node node = addWaiter(Node.SHARED);
boolean failed = true;
try {
// 注意这里的循环!!!
for (;;) {
// 找到上个节点
final Node p = node.predecessor();
if (p == head) {
// 尝试获取锁
int r = tryAcquireShared(arg);
// 如果成功了的话,就可以不用 park 了
if (r >= 0) {
setHeadAndPropagate(node, r);
p.next = null; // help GC
failed = false;
return;
}
}
/**
* 第一个方法判断在锁请求失败之后,是否应该 park,
* 第二个方法则 park 当前线程;
**/
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
throw new InterruptedException();
}
} finally {
if (failed)
cancelAcquire(node);
}
}
共享锁请求失败后,进入上面这个方法,创建节点,入队列;在节点创建成功,入队列之后,线程被 park 之前,需要判断是否能够成功获取锁,这样的话,就不用 park 了;为什么要用 p == head
作为获取锁的 if 条件呢?想想看,队列是有序的,如果上一个节点都还在 park 状态,那么当前节点是不是不能抢在它之前 ,提前结束掉!这也是用于当前线程在 park 结束后,重新唤醒其后继节点的线程的;
shouldParkAfterFailedAcquire
这个方法修改节点的状态并优化队列:
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus;
if (ws == Node.SIGNAL)
/*
* This node has already set status asking a release
* to signal it, so it can safely park.
*/
return true;
if (ws > 0) {
/*
* Predecessor was cancelled. Skip over predecessors and
* indicate retry.
*/
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
/*
* waitStatus must be 0 or PROPAGATE. Indicate that we
* need a signal, but don't park yet. Caller will need to
* retry to make sure it cannot acquire before parking.
*/
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
修改上一节点的 waitStatus
为 Node.SIGNAL
,所以当前线程在进入doAcquireSharedInterruptibly
中的 for 循环时,会在这里执行失败,无法 park,直到下一次循环,判断 waitStatus
的 值为 Node.SIGNAL
才返回 true
,才会执行使当前线程 park 的 parkAndCheckInterrupt
方法。
现在队列是什么状态?两个节点,一个空的头节点,waitStatus
为 Node.SIGNAL
,一个线程被 park 的节点,waitStatus
为 初始值 0,如果再进来一个线程,新建了一个节点,那么队列变为 “ 头节点不变,前一个线程被 park 的节点的 waitStatus
值变为 1,新来的线程这个节点链接在其后,waitStatus
为 0 ”;
下面看看释放操作 doReleaseShared
方法:
private void doReleaseShared() {
/*
* Ensure that a release propagates, even if there are other
* in-progress acquires/releases. This proceeds in the usual
* way of trying to unparkSuccessor of head if it needs
* signal. But if it does not, status is set to PROPAGATE to
* ensure that upon release, propagation continues.
* Additionally, we must loop in case a new node is added
* while we are doing this. Also, unlike other uses of
* unparkSuccessor, we need to know if CAS to reset status
* fails, if so rechecking.
*/
// 注意循环
for (;;) {
Node h = head;
// 从头节点开始
if (h != null && h != tail) {
int ws = h.waitStatus;
if (ws == Node.SIGNAL) {
// 第一次执行成功,修改 waitStatus 为 0
if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
continue; // loop to recheck cases
// 唤醒后继节点线程
unparkSuccessor(h);
}
// 跳过
else if (ws == 0 &&
!compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
continue; // loop on failed CAS
}
// 结束,如果头节点引用未被改变
if (h == head) // loop if head changed
break;
}
}
从头节点开始,唤醒后继节点,这里主要看看 unparkSuccessor
方法:
private void unparkSuccessor(Node node) {
/*
* If status is negative (i.e., possibly needing signal) try
* to clear in anticipation of signalling. It is OK if this
* fails or if status is changed by waiting thread.
*/
int ws = node.waitStatus;
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
/*
* Thread to unpark is held in successor, which is normally
* just the next node. But if cancelled or apparently null,
* traverse backwards from tail to find the actual
* non-cancelled successor.
*/
Node s = node.next;
// 如果后继节点不存在,或者等待状态值大于0,则倒过来开始从尾节点开始找
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;
}
// 节点线程被 unpark
if (s != null)
LockSupport.unpark(s.thread);
}
节点线程被 unpark
,doAcquireSharedInterruptibly
方法中的 for 循环会重新进入:
// 省略了其它代码
// 注意这里的循环!!!
for (;;) {
// 找到上个节点
final Node p = node.predecessor();
if (p == head) {
// 尝试获取锁
int r = tryAcquireShared(arg);
// 如果成功了的话,就可以不用 park 了
if (r >= 0) {
setHeadAndPropagate(node, r);
p.next = null; // help GC
failed = false;
return;
}
}
/**
* 第一个方法判断在锁请求失败之后,是否应该 park,
* 第二个方法则 park 当前线程;
**/
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
throw new InterruptedException();
这个时候,tryAcquireShared
会返回 1,一个大于 0 的数,所以重要的 setHeadAndPropagate
方法来了,先看下现在的队列情况:空的头节点,ws=0
,第一个线程节点,ws = Node.SIGNAL
,线程已经 unpark,第二个线程节点,ws=0
,线程还处在 park;
private void setHeadAndPropagate(Node node, int propagate) {
Node h = head; // Record old head for check below
setHead(node);
if (propagate > 0 || h == null || h.waitStatus < 0 ||
(h = head) == null || h.waitStatus < 0) {
Node s = node.next;
if (s == null || s.isShared())
doReleaseShared();
}
}
这个时候将第一个线程节点设置为 头节点,thread 为 null,propagate
值 会大于1,下一个线程将会被唤醒,通过重新调用 doReleaseShared()
,因为这也会将上面整个流程重新走一遍。
还记得 doReleaseShared
方法中的 for 循环结束条件吧!
for (;;) {
// 省略其它代码
if (h == head) // loop if head changed
break;
}
在执行到第一个线程被修改为 头节点时,这里的循环就有可能结束不掉了,所以 doReleaseShared
方法会接着执行,这和 setHeadAndPropagate
方法中的该方法执行一样!真的好巧妙啊!
总结
写了两个多小时,流程真的很复杂,算是有交互了,不过也算理清楚了,搞明白几个基础理论,几个关键方法,也能略知一二啦!
推荐博文
参考博文
我与风来
认认真真学习,做思想的产出者,而不是文字的搬运工
错误之处,还望指出