共计 14278 个字符,预计需要花费 36 分钟才能阅读完成。
AQS(AbstractQueuedSynchronizer) 源码剖析
阐明
- 本文基于 jdk 8 写作。
- @author JellyfishMIX – github / blog.jellyfishmix.com
- LICENSE GPL-2.0
AQS 须要关注的点有哪些?
- AQS 全称 AbstractQueuedSynchronizer,是 juc 包 (java.util.concurrent) 中一个同步器开发框架,用于反对下层的锁。
- 要害的属性:state 同步状态,CHL 同步队列。
- 两种模式:独占模式,共享模式。
AQS 的要害属性
- sate 同步状态,是锁的表述,示意有多少线程获取了锁。
- CHL 队列(同步队列),由链表实现,以 CAS + 自旋的形式获取资源,是可阻塞的先进先出的双向队列。通过 CAS + 自旋保障节点插入和移除的原子性。当有线程获取锁失败,就被增加到队列开端。当有线程开释了锁,会从 CHL 队头出队一个线程。
state 同步状态
/**
* The synchronization state.
*/
private volatile int state;
AQS 锁反对的子类,能够借助操作同步状态 state 来实现尝试获取同步状态 (tryAcquire)、尝试开释同步状态(tryRelease) 的逻辑。
也就是说,AQS 的子类 (各种锁) 判断何时能获取同步状态,何时能开释同步状态,须要借助于判断 state 的值。而后能把获取同步状态进一步封装成获取锁(lock),把开释同步状态封装成开释锁(unlock)。至于 state 为何值时能够获取同步状态,state 为何值时能够开释同步状态,这些都是交由子类判断的。
AQS 只是当子类判断为未获取到同步状态时,通过 CHL 入队操作,把线程退出到同步队列中,而后让线程休眠。当子类判断为能够开释同步状态时,从 CHL 同步队列中唤醒一个线程。
CHL 队列
在剖析前,可能会有如下纳闷:
- 节点的数据结构是什么样的?
- 是单向还是双向?
- 有无头节点和尾节点?
Node
CHL 是链表,链表是由节点 node 组成的。
static final class Node {
/**
* 标记一个节点正在共享模式下期待
*/
static final Node SHARED = new Node();
/**
* 标记一个节点正在独占模式下期待
*/
static final Node EXCLUSIVE = null;
/**
* waitStatus value,标记以后节点的线程被 cancel 勾销
*/
static final int CANCELLED = 1;
/**
* waitStatus value,标记后继节点的线程须要被唤醒
*/
static final int SIGNAL = -1;
/**
* waitStatus value,标记以后节点的线程进入期待队列中
*/
static final int CONDITION = -2;
/**
* waitStatus value,示意下一次共享式的同步状态获取将会无条件流传上来
*/
static final int PROPAGATE = -3;
/**
* 节点的状态
*/
volatile int waitStatus;
/**
* 以后节点的前驱节点
*/
volatile Node prev;
/**
* 以后节点的后继节点
*/
volatile Node next;
/**
* 以后节点援用的线程
*/
volatile Thread thread;
/**
* 期待队列中的下一个节点
*/
Node nextWaiter;
}
留神到属性 prev, next, 阐明 CHL 是一个双向队列。
AQS 中有两个属性,用作头尾指针,通过头尾指针来治理同步队列。头尾指针应用场景包含获取锁失败的线程进行入队,开释锁时对同步队列中的线程进行告诉等外围办法。
/**
* 指向 CHL 队列的头节点
*/
private transient volatile Node head;
/**
* 指向 CHL 队列的尾节点
*/
private transient volatile Node tail;
CHL 队列中 node 的出队和入队操作,实际上对应着同步状态的获取和开释两个操作:获取同步状态失败 -> 进行入队操作,获取同步状态胜利 -> 进行出队操作。
独占模式与共享模式
- AQS 的工作模式分为独占模式 (EXCLUSIVE) 和共享模式(SHARED),记录在 node 的信息中。
- 独占模式:同一时间只有一个线程能拿到锁执行,锁的 state 只有 0 和 1 两种状况(ReentrantLock 的可重入锁是个例外)。典型的实现如 ReentrantLock。
- 共享模式:同一时间有多个线程能够拿到锁执行,锁的 state 大于或等于 0。典型的实现如 CountDownLunch。
- 它的实现类 (子类) 中,要么实现并应用了它独占性能的 API,要么应用了共享性能的 API,而不会同时应用两套 API。即使是它有名的子类 ReentrantReadWriteLock,也是通过两个外部类:读锁和写锁,别离实现的两套 API 来实现的。
- AQS 应用了模板办法设计模式,定义一套操作逻辑,相当于一个算法的骨架,而将一些步骤的实现提早到子类中,比方获取、开释 state 同步状态。
独占模式
以下是独占模式的办法
/* 独占式获取同步状态,如果获取失败则插入同步队列进行期待 */
void acquire(int arg);
/* 与 acquire 办法雷同,但在同步队列中进行期待的时候能够检测中断 */
void acquireInterruptibly(int arg);
/* 在 acquireInterruptibly 根底上减少了超时期待性能,在超时工夫内没有取得同步状态返回 false */
boolean tryAcquireNanos(int arg, long nanosTimeout);
/* 开释同步状态,该办法会唤醒在同步队列中的下一个节点 */
boolean release(int arg);
acquire 办法(独占模式获取同步状态)
/**
* Acquires in exclusive mode, ignoring interrupts. Implemented
* by invoking at least once {@link #tryAcquire},
* returning on success. Otherwise the thread is queued, possibly
* repeatedly blocking and unblocking, invoking {@link
* #tryAcquire} until success. This method can be used
* to implement method {@link Lock#lock}.
*
* 模板办法,封装了线程获取资源失败后,进入同步队列并阻塞的逻辑
*
* @param arg the acquire argument. This value is conveyed to
* {@link #tryAcquire} but is otherwise uninterpreted and
* can represent anything you like.
*/
public final void acquire(int arg) {
// tryAcquire 办法由子类实现,尝试获取同步状态
if (!tryAcquire(arg) &&
// 未获取到同步状态,则进入同步队列
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
// 如果在同步队列中产生中断,则中断标记为 true,执行 selfInterrupt() 办法
selfInterrupt();}
咱们把 acquire 办法概括一下:
// 交由子类判断是否能胜利获取到同步状态
if (!tryAcquire(arg) &&
// 未获取到同步状态,则调用 addWaiter 办法把线程构建成 node,追加到 CHL 同步队列尾部。// 追加至 CHL 同步队列后,调用 acquireQueued 办法让线程休眠,此办法也蕴含了线程昏迷后从新抢同步状态的逻辑。acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
// 如果在休眠过程中产生中断,则中断标记为 true,执行 selfInterrupt() 办法
selfInterrupt();
- 先调用 tryAcquire 办法(由子类实现,获取同步状态),判断是否获取胜利。
- 如果获取胜利,acquire 办法返回完结。如果获取失败,调用 addWaiter 办法,把以后线程构建成 node,把 node 追加至 CHL 同步队列的尾部。
- node 追加至 CHL 队列尾部后,调用 acquireQueued 办法,把以后线程休眠。同时 acquireQueued 外面也包含了线程休眠被唤醒后尝试抢锁的逻辑。
acquire 办法流程图:
addWaiter 办法
/**
* Creates and enqueues node for current thread and given mode.
*
* 依据传入的独占 / 共享模式,在 CHL 队列中增加一个 node
*
* @param mode Node.EXCLUSIVE for exclusive, Node.SHARED for shared
* @return the new node
*/
private Node addWaiter(Node mode) {
// 依据传入的独占 / 共享模式,结构一个新 node
Node node = new Node(Thread.currentThread(), mode);
// Try the fast path of enq; backup to full enq on failure
/*
* 上面这个 if 分支里在做的事件,和 enq 办法中在做的事件雷同。能够了解为,把 enq 中的局部代码放在了里面。* 这种做法在并发编程中还挺常见,把最有可能胜利执行的代码间接写在最罕用的调用处。* 在线程数不多的状况下,CAS 还是很难失败的,因而这种写法能够节俭多条指令。比方节俭了办法调用、enq 办法中进入循环的开销。* 是 jdk 中一种极致的优化,业务代码能够不必做到这么极致。*/
Node pred = tail;
// 如果队尾不为空(即以后队列中有元素),则尝试在队尾追加 node
if (pred != null) {
/*
* node.prev = pred 这里其实是有线程平安问题的,并发状况下可能有多个刚结构进去的 node.prev 被设置为了 pred。* 然而没关系,前面 CAS 设置队尾的时候会失败,而后进入 enq 办法中循环去做。当某次 CAS 胜利时,阐明 CAS 前的那步 node.prev 赋值也正确了。* 所以,这里只是部分存在线程平安问题,最终后果是平安的。* 为什么设置尾节点时都要先将之前的尾节点设置为 node.prev 的值呢,而不是在 CAS 之后再设置?* 因为如果不提前设置 node.prev 的话,在 CAS 设置完 tail 后会存在一瞬间的 tail.pre 为 null 的状况,而 Doug Lea 正是思考到这种状况,不管何时获取 tail.pre 都不会为 null
*/
node.prev = pred;
// CAS 尝试设置尾节点为以后节点
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
// 尝试循环在队尾追加 node
enq(node);
return node;
}
enq 办法
/**
* Inserts node into queue, initializing if necessary. See picture above.
*
* 尝试循环在队尾追加 node
*
* @param node the node to insert
* @return node's predecessor
*/
private Node enq(final Node node) {for (;;) {
Node t = tail;
// 如果 tail 为空,阐明队列中还没有元素,须要初始化一个头节点
if (t == null) { // Must initialize
if (compareAndSetHead(new Node()))
tail = head;
} else {
/*
* node.prev = t 这里其实是有线程平安问题的,并发状况下可能有多个刚结构进去的 node.prev 被设置为了 t。* 然而没关系,下一步 CAS 设置队尾的时候会失败,而后进入下一次循环。当某次 CAS 胜利时,阐明 CAS 前的那步 node.prev 赋值也正确了。* 所以,这里只是部分存在线程平安问题,最终后果是平安的。* 为什么设置尾节点时都要先将之前的尾节点设置为 node.prev 的值呢,而不是在 CAS 之后再设置?* 因为如果不提前设置 node.prev 的话,在 CAS 设置完 tail 后会存在一瞬间的 tail.pre 为 null 的状况,而 Doug Lea 正是思考到这种状况,不管何时获取 tail.pre 都不会为 null
*/
node.prev = t;
// CAS 尝试设置尾节点为以后节点
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
addWaiter 办法的逻辑:把以后线程构建成 node,把 node 追加至 CHL 同步队列的尾部。粗略地了解,咱们能够把 enq 办法合并至 addWaiter 办法中,enq 办法在承当 addWaiter 办法的一部分逻辑。咱们把次要逻辑概括一下:
// 依据传入的独占 / 共享模式,结构一个新 node
for (;;) {
// CAS 操作尝试向 CHL 队列尾部追加此 node。CAS 操作胜利向队尾追加后,跳出循环。// CAS 操作如果失败,则进行下一次的循环。}
咱们留神到,addWaiter 办法 和 enq 办法中,有局部代码做的事件雷同。能够了解为,把 enq 中的局部代码放在了里面。这种做法在并发编程中还挺常见,把最有可能胜利执行的代码间接写在最罕用的调用处。在线程数不多的状况下,CAS 还是很难失败的,因而这种写法能够节俭多条指令。比方节俭了办法调用、enq 办法中进入循环的开销。这种写法是 jdk 中一种极致的优化,业务代码能够不必做到这么极致。
咱们还能够留神到 node.prev = t; 放在了 if CAS 之前,这里其实是有线程平安问题的,并发状况下可能有多个刚结构进去的 node.prev 被设置为了 t。然而没关系,下一步 CAS 设置队尾的时候会失败,而后进入下一次循环。当某次 CAS 胜利时,阐明 CAS 前的那步 node.prev 赋值也正确了。所以,这里只是部分存在线程平安问题,最终后果是平安的。
为什么设置尾节点时都要先将之前的尾节点设置为 node.prev 的值呢,而不是在 CAS 之后再设置?因为如果不提前设置 node.prev 的话,在 CAS 设置完 tail 后会存在一瞬间的 tail.pre 为 null 的状况,而 Doug Lea 正是思考到这种状况,不管何时获取 tail.pre 都不会为 null
acquireQueued 办法
咱们回到 acquire 办法中,去看 acquireQueued 办法。
/**
* Acquires in exclusive uninterruptible mode for thread already in
* queue. Used by condition wait methods as well as acquire.
*
* 在 CHL 队列中的线程获取同步状态的逻辑,是 CHL 队列中的线程获取同步状态 / 睡眠的外围逻辑
*
* @param node the node
* @param arg the acquire argument
* @return {@code true} if interrupted while waiting
*/
final boolean acquireQueued(final Node node, int arg) {
// 获取同步状态是否胜利
boolean failed = true;
try {
// 中断标记
boolean interrupted = false;
// 循环尝试获取同步状态
for (;;) {
// 以后节点的前驱节点
final Node p = node.predecessor();
// 判断以后节点的前驱节点是不是头节点,如果是则阐明应该出队了,调用 tryAcquire 办法(子类实现),获取同步状态
if (p == head && tryAcquire(arg)) {
// 同步状态获取胜利,把以后节点设置为头节点
setHead(node);
// 把老的头节点开释掉(通过去除老头节点的援用关系,最终让 GC 去回收此对象)p.next = null; // help GC
failed = false;
// 返回中断标记
return interrupted;
}
// 如果走到这一步,阐明以后节点不是头节点,或未获取到同步状态,阐明获取同步状态失败(这种状况存在于其余线程曾经领先获取了同步状态)。判断是否须要休眠。if (shouldParkAfterFailedAcquire(p, node) &&
// 进入睡眠状态,判断在睡眠状态中是否被中断
parkAndCheckInterrupt())
// 如果在睡眠状态中被中断,则设置中断标记为 true
interrupted = true;
}
} finally {
// 比方以后节点因为超时或响应了中断,须要勾销本人
if (failed)
// 勾销获取同步状态
cancelAcquire(node);
}
}
acquireQueued 办法是 CHL 队列中的线程获取同步状态 / 睡眠的外围逻辑。咱们把次要逻辑概括一下:
for (;;) {
// 尝试获取同步状态,抢到同步状态则跳出循环
// 未抢到同步状态,让线程休眠
}
在进入休眠前,尝试再获取一次同步状态,如果依然失败,则会调用 shouldParkAfterFailedAcquire 办法,判断是否须要休眠。
shouldParkAfterFailedAcquire 办法
/**
* Checks and updates status for a node that failed to acquire.
* Returns true if thread should block. This is the main signal
* control in all acquire loops. Requires that pred == node.prev.
*
* 查看未获取到同步状态的 node 的状态,并更新 status。* 如果线程应该阻塞,返回 true。这是在所有循环获取同步状态中,次要的信号管制。要求 pred == node.prev
*
* @param pred node's predecessor holding status
* @param node the node
* @return {@code true} if thread should block
*/
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
// 获取以后节点的前驱节点的状态
int ws = pred.waitStatus;
// 如果前驱节点的状态为 SIGNAL,示意前驱节点的后继节点(即以后节点)的线程须要被唤醒
if (ws == Node.SIGNAL)
/*
* This node has already set status asking a release
* to signal it, so it can safely park.
*
* 以后节点曾经把前驱节点设置为了 SIGNAL,示意以后节点须要被唤醒,所以它能够平安地阻塞(休眠)*/
return true;
// 如果前驱节点的状态 > 0(即 CANCELLED 状态)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.
*
* 在休眠前须要给前驱节点设置一个标识,将 waitStatus 设置为 Node.SIGNAL(-1)
*/
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
在 shouldParkAfterFailedAcquire 办法中,须要过滤有效的前驱节点(比方曾经被勾销的前驱节点),而后把无效的前驱节点设置为 SIGNAL,示意前驱节点的下一个节点(即以后节点)须要被唤醒,而后它能够平安地阻塞(休眠)。
在之前的 acquireQueued 办法中,调用 shouldParkAfterFailedAcquire 办法判断是否须要休眠。如果须要休眠,则会调用 parkAndCheckInterrupt 办法,进行休眠。
parkAndCheckInterrupt 办法
/**
* Convenience method to park and then check if interrupted
*
* 以后线程进入休眠状态,返回值示意休眠过程中是否产生了中断
*
* @return {@code true} if interrupted
*/
private final boolean parkAndCheckInterrupt() {
// 以后线程休眠
LockSupport.park(this);
// 重置线程的中断标记为 false,并返回一个 boolean 值示意线程的中断标记被重置前的值(true/false)
return Thread.interrupted();}
如果在休眠过程中产生中断,中断标记会被设置为 true,而后持续在 acquireQueued 的自旋 for(;;)
中尝试获取同步状态,获取同步状态后,acquireQueued 办法返回。这也阐明了,即便线程在休眠过程中产生了中断,也只有在线程获取到同步状态后,能力响应中断。如果在休眠过程中没有产生中断,则中断标记仍然是 false。
acquireQueued 的返回值是一个中断标记,示意在休眠过程中是否产生中断。咱们再从新看一下 acquire 办法。
/**
* Acquires in exclusive mode, ignoring interrupts. Implemented
* by invoking at least once {@link #tryAcquire},
* returning on success. Otherwise the thread is queued, possibly
* repeatedly blocking and unblocking, invoking {@link
* #tryAcquire} until success. This method can be used
* to implement method {@link Lock#lock}.
*
* 模板办法,封装了线程获取资源失败后,进入同步队列并阻塞的逻辑
*
* @param arg the acquire argument. This value is conveyed to
* {@link #tryAcquire} but is otherwise uninterpreted and
* can represent anything you like.
*/
public final void acquire(int arg) {
// tryAcquire 办法由子类实现,尝试获取同步状态
if (!tryAcquire(arg) &&
// 未获取到同步状态,则进入同步队列
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
// 如果在同步队列中产生中断,则中断标记为 true,执行 selfInterrupt() 办法
selfInterrupt();}
如果在同步队列中产生中断,则中断标记为 true,执行 selfInterrupt() 办法。
selfInterrupt() 办法
/**
* Convenience method to interrupt current thread.
*
* 以后线程中断本人
*/
static void selfInterrupt() {Thread.currentThread().interrupt();}
相当于在 acquire 办法中,让以后线程本人中断本人。咱们在 acquire 办法的调用处,或者更下层的调用处,就能进行中断的捕捉和解决。
acquireQueued 办法的诘问 — 为什么 acquire 办法要靠 acquireQueued 办法的返回值判断,来调用 .interrupt() 办法
这里的诘问探索的比拟粗疏,如果不感兴趣间接跳过即可,对了解次要逻辑影响不大。
acquireQueued 办法会返回一个中断标记(acquireQueued 办法中的局部变量 interrupted,以下简称为中断标记 interrupted),示意是否产生中断。acquireQueued 办法返回后,回到了 acquire 办法:
public final void acquire(int arg) {if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();}
如果中断标记 interrupted 为 true,则会执行 selfInterrupt() 办法,也就是执行:
Thread.currentThread().interrupt();
为什么要做的这么麻烦呢,还要等 acquireQueued 办法返回之后,依据中断标记来做中断。咱们间接在 acquireQueued 办法中去执行 Thread.currentThread().interrupt();
不行吗?
咱们来回顾一下 acquireQueued 的源码,尝试在 1 号地位 和 2 号地位加 Thread.currentThread().interrupt();
,看看能不能防止应用 interrupted 这个返回值。
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;
// 2 号地位
return interrupted;
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
// 1 号地位
interrupted = true;
}
} finally {if (failed)
cancelAcquire(node);
}
}
1 号地位加 Thread.currentThread().interrupt();
不能够。如果 .interrupt() 办法在 1 号地位被调用了,以后 Thread 的中断标记会被被置为 true。如果下一次循环中,线程没有获取到同步状态,会走让线程休眠的逻辑。而让线程休眠的办法 parkAndCheckInterrupt 调用的是 LockSupport.park
private final boolean parkAndCheckInterrupt() {
// 以后线程休眠
LockSupport.park(this);
// 重置线程的中断标记为 false,并返回一个 boolean 值示意线程的中断标记被重置前的值(true/false)
return Thread.interrupted();}
LockSupport.park 办法会让线程进入休眠状态,然而以后 Thread 的中断标记为 true 时,LockSupport.park 会立即返回,不会产生阻塞。这就很危险了,如果线程始终无奈获取到同步状态,acquireQueued 办法中
for (;;) {
// 尝试获取同步状态,抢到同步状态则跳出循环(办法返回)// 未抢到同步状态,让线程休眠
}
for (;;)
循环无奈休眠,会始终空转,对 cpu 性能耗费会非常重大。
这也是为什么在让未获取到同步状态的线程休眠时,调用的办法 parkAndCheckInterrupt 中,在 LockSupport.park 办法完结后,会调用 Thread.interrupted() 办法重置线程的中断标记。
这里插入一下对于线程的中断标记 (interrupt flag) 的介绍:
什么是 Thread 的中断标记(interrupt flag)?
中断标记(interrupt flag) 是线程产生中断时,设置的外部属性。在 Thread.java 中是看不到这个 interrupt flag 属性的,因为它写在了 c++ 的代码中,底层设置中断标记的办法 private native void interrupt0() 是一个 native 办法。咱们只须要晓得,中断标记(interrupt flag) 用来示意线程是否产生了中断。
怎么设置中断标记?
要设置一个线程的中断标记,只须要简略的在线程对象上调用 thread.interrupt() 办法。
怎么革除中断标记?
Thread.interrupted() 办法能够重置中断标记为 false,并返回一个 boolean 值示意中断标记被重置前的值(true/false)。
2 号地位加 Thread.currentThread().interrupt();
也不能够。咱们看一下 AQS 中 acquireQueued 办法的调用处,能够看到有好几处中央用到了 acquireQueued 办法:
咱们选一个调用处来看看:
能够看到,在这里,依据 acquireQueued 办法的返回值(中断标记 interrupted),进行了别的一些解决,并没有间接调用Thread.currentThread().interrupt()
。所以咱们在 acquireQueued 办法外部 2 号地位加 Thread.currentThread().interrupt()
去管制以后线程的中断标记 interrupt flag 也是不适合的。
至此,咱们解释了问题:acquireQueued 办法的诘问 — 为什么 acquire 办法要靠 acquireQueued 办法的返回值判断,来调用 .interrupt() 办法。
release 办法(独占模式开释同步状态)
release 是 AQS 独占模式开释同步状态的办法。
/**
* Releases in exclusive mode. Implemented by unblocking one or
* more threads if {@link #tryRelease} returns true.
* This method can be used to implement method {@link Lock#unlock}.
*
* 模版办法,其中 tryRelease 办法由子类实现。以后锁开释指定的 state 数量,并返回锁是否开释胜利。*
* @param arg the release argument. This value is conveyed to
* {@link #tryRelease} but is otherwise uninterpreted and
* can represent anything you like.
* @return the value returned from {@link #tryRelease}
*/
public final boolean release(int arg) {
// tryRelease 办法由子类实现,尝试开释同步状态
if (tryRelease(arg)) {
Node h = head;
// 判断边界条件,而后把 CHL 同步队列中头节点的后继节点唤醒
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
这是一个模板办法,次要做了两件事件。一个是调用子类实现的办法去尝试开释同步状态。一个是把队列中头节点的后继节点唤醒。
AQS 唤醒 CHL 同步队列中的后续节点,调用了 unparkSuccessor 办法。
unparkSuccessor 办法
/**
* Wakes up node's successor, if one exists.
*
* 如果以后节点的后继节点存在,则唤醒它
*
* @param node the node
*/
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.
*/
// node 的状态
int ws = node.waitStatus;
// 如果以后节点的状态不是被勾销,则把以后节点的状态置为 0,能够了解为重置以后节点的状态(之前的状态可能是用于批示后继节点的,比方 SIGNAL)。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;
// 如果以后节点的后继节点为空,或者后继节点被勾销
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;
}
// 以后节点的后继节点不为 null 时,唤醒以后节点的后继节点
if (s != null)
LockSupport.unpark(s.thread);
}
unparkSuccessor 办法在做的事件是,如果以后节点的后继节点存在,则唤醒它。大部分代码在做边界条件的解决,真正唤醒线程应用的 LockSupport.unpark(thread) 办法。
LockSupport.unpark(thread) 这个办法咱们能够临时不必关注。它外面调用了 UNSAFE.unpark(thread),UNSAFE 类是一个很底层的类,并且是闭源的,不举荐一般开发人员在业务代码中间接调用的,个别是框架底层一些的实现会去应用。咱们只须要晓得,LockSupport.unpark(thread) 能够唤醒一个线程即可。
未完待续
本文目前仅剖析了 AQS 中,独占模式获取同步状态的逻辑,以下内容待欠缺:
- AQS 中共享模式获取同步状态的逻辑。
- AQS 中共享模式开释同步状态的逻辑。
- CHL 队列为什么是双向的?(剖析中曾经体现了,打算在文章中概括答复)
工夫无限,未完待续 …