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 办法的一部分逻辑。咱们把次要逻辑概括一下:
// 依据传入的独占/共享模式,结构一个新 nodefor (;;) { // 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 队列为什么是双向的?(剖析中曾经体现了,打算在文章中概括答复)
工夫无限,未完待续...