AQS 原理
AQS(AbstractQueuedSynchronizer)
JUC 的基石
- 提供加锁释放锁的模板方法,子类根据自身自定义实现,比如公平/非公平、独占/共享 等
- 共性的操作自己内部已经实现好了,不需要子类再实现,比如线程入队、唤醒、CAS原子修改锁状态等
核心属性
成员变量
// 队列头节点(根据节点属性确定是条件队列的节点还是同步队列节点)
private transient volatile Node head;
// 队列尾节点(根据节点属性确定是条件队列的节点还是同步队列节点)
private transient volatile Node tail;
// 锁状态(state=0:未锁定,state>0:锁定或重入;读写锁中低16位表示写锁,高16位表示读锁)
private volatile int state;
内部类
// Node 节点(同步队列和条件队列都是用 Node 作为元素)
static final class Node {
/**
* 处于不同的队列时,含义也不一样
* 1)如果当前节点处于同步队列,要么是 SHARED(共享,new Node),要么是 EXCLUSIVE(独占,null)
* 2)如果当前节点处于条件队列,要么指向一个 Node(后继节点),要么指向 null(尾节点)
* 如果是 null 怎么区分当前节点是同步队列的独占锁还是条件队列的尾节点呢?通过 waitStatus 属性来区分
*/
Node nextWaiter;
static final Node SHARED = new Node(); // 线程正以共享模式等待锁
static final Node EXCLUSIVE = null; // 线程正以独占模式等待所
/**
* 节点状态(不要和 AQS 的 state 搞混了,这是节点的状态,AQS 的 state 表示锁状态),有 5 个候选值如下:
* 1) 0(int默认值) :节点刚进入同步队列,不管是正常入队还是条件队列转入同步队列,都不会设置值,所以是 int 的默认值 0
* 2) 1(CANCELLED):线程取消等待,这个节点将被清除(比如中断、超时等场景)
* 3)-1(SIGNAL) :表示需要唤醒后继节点,此时节点处于同步队列(但是同步队列的节点不一定是-1)
* 4)-2(CONDITION):节点处于条件队列,初始入条件队列时就设置为了-2
* 5)-3(PROPAGATE):共享模式的专用状态,用于优化唤醒操作,减少不必要的线程竞争
*/
volatile int waitStatus;
static final int CANCELLED = 1;
static final int SIGNAL = -1;
static final int CONDITION = -2;
static final int PROPAGATE = -3;
volatile Node prev; // 同步队列前驱节点
volatile Node next; // 同步队列后置节点
volatile Thread thread; // 绑定的线程
}
// 条件变量
public class ConditionObject implements Condition, java.io.Serializable {
// 条件队列首尾指针,使用的元素也是 Node
private transient Node firstWaiter;
private transient Node lastWaiter;
// 唤醒当前节点
public final void signal() {}
// 阻塞当前节点
public final void await() throws InterruptedException {}
}
核心方法
核心是这 4 个方法,每个方法都会有 3 个事情要处理:锁、队列、线程
队列和线程的维护都是有共性的,所以 AQS 里已经实现好了,子类拿来即用
锁的维护需要子类自己实现,这里就不展开说了
| 方法 | 作用 | 描述 |
|---|---|---|
acquire(int arg) |
独占模式获取资源 | |
release(int arg) |
独占模式释放资源 | |
acquireShared(int arg) |
共享模式获取资源 | state 变量低 16 位表示写锁,高 16 位表示读锁 |
releaseShared(int arg) |
共享模式释放资源 | state 变量低 16 位表示写锁,高 16 位表示读锁 |
原理分析
独占模式加锁
public final void acquire(int arg) {
if (!tryAcquire(arg) && // 公平锁 or 非公平锁,子类实现
acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) // 加锁不成功线程入队并挂起线程
selfInterrupt();
}
tryAcquire() 是一个模板方法,由子类具体实现,AQS 中只管处理的结果,返回 true 表示加锁成功,返回 false 说明加锁失败
!tryAcquire(arg) 是 true 表示子类加锁失败,AQS 处理后续的操作(&& 操作符,所以执行 acquireQueued(addWaiter(Node.EXCLUSIVE), arg)))
线程入队
就是 addWaiter() 方法的作用,源码如下:
private Node addWaiter(Node mode) {
Node node = new Node(Thread.currentThread(), mode); // 线程封装成 Node(model 传的是独占模式)
// 如果尾节点不为空:维护链表关系,新节点加入队尾就行了
Node pred = tail;
if (pred != null) {
node.prev = pred;
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node; // 返回节点
}
}
enq(node); // 如果尾节点为空的处理
return node;
}
/**
* 当前节点是第一个入队,这时队列肯定是空的,首尾节点也是指向空
* 1)第一次循环时,初始化队列,初始化的做法也比较简单,就是首尾指针都指向 new 出来的一个节点(虚拟节点)
* 2)第二次循环时,这时队列就不为空了,第一次循环时处理好了首尾节点,这次循环的目的就是让当前节点入队
*/
private Node enq(final Node node) {
for (;;) {
Node t = tail;
if (t == null) { // 尾节点为空(队列为空),进行初始化
if (compareAndSetHead(new Node()))
tail = head;
} else { // 如果不为空,节点入队
node.prev = t; // 双向
if (compareAndSetTail(t, node)) { // 更新尾节点
t.next = node; // 双向
return t;
}
}
}
}
入队的逻辑就结束了,还是比较简单的,有两点需要明确一下
头结点是虚拟节点。为什么是虚拟节点?
一旦虚拟节点创建了,队列中就永远存在一个虚拟节点,队列也就不会为空,每次有新节点出入队时,不用每次都判空
比如 enq 方法只会调用一次,因为一旦调用后,队列永远都不会为空了
入队的节点只设置了 nextWaiter 和 thread,其他值都没设置,waitStatus 此时是 int 的默认值 0
// 使用这个构造方法来创建进入同步队列的节点,只维护了两个值
Node(Thread thread, Node mode) {
this.nextWaiter = mode; // 设置 nextWaiter = Node.EXCLUSIVE
this.thread = thread; // 设置线程
}
线程阻塞
就是 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)) { // 能获取到,那么更新队列,当前节点作为新的虚拟节点
setHead(node); // thread = null,prev = null,head = node
p.next = null; // 前驱节点断开链表
failed = false;
return interrupted;
}
// 如果前驱节点不是头结点或获取锁失败,这两个情况若一个为真,当前节点都不应该获取锁,应该阻塞住
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt()) // 调用 LockSupport.park() 阻塞线程
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
/**
* java.util.concurrent.locks.AbstractQueuedSynchronizer#shouldParkAfterFailedAcquire
* 1)如果线程第一次入队,前驱节点是虚拟节点,是 new Node() 出来的,各个属性值没有赋值,此时前驱节点的 waitStatus 就是 0
* 2)如果线程非第一次入队,前驱节点就是普通节点,正常情况下waitStatus是0,如果被取消就是1
*
* 不管是不是第一次入队,这个方法都没有修改待入队节点的任何值,所以待入队节点的waitStatus任然是0
*
* 这个方法做2件事:1 已取消的节点出队;2 前驱节点改为 -1(前驱节点不可能是虚拟头结点)
*/
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus; // 前驱节点状态
if (ws == Node.SIGNAL) // 前驱节点已经是 -1,直接阻塞当前线程(外层方法处于无限循环,下面改为 -1 后再次进入循环时条件为真)
return true;
if (ws > 0) { // 前驱节点已取消(1),跳过它(>1 的节点都会断开链表)
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
// 将前驱节点的 waitStatus CAS 改为 -1
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
独占模式释放锁
public final boolean release(int arg) {
if (tryRelease(arg)) { // 子类实现如何释放锁,如果释放成功,继续执行
// 如果头结点为空,说明队列是空,因为只要有线程入队过,队列中始终会存在一个虚拟节点
Node h = head;
if (h != null && h.waitStatus != 0) // 队列不为空且才唤醒(如果是头结点waitStatus是-1,入队节点把前驱节点改成的-1)
unparkSuccessor(h); // 唤醒线程
return true;
}
return false;
}
同加锁一样,具体释放锁由子类实现,AQS 只是处理具有共性的操作,比如出入队线程阻塞唤醒
线程唤醒
加锁时这两个操作对应两个方法,释放锁时这两个操作在一个方法中 unparkSuccessor(h),源码如下:
private void unparkSuccessor(Node node) { // 传进来的是头结点
// 如果状态 < 0,改为 0
int ws = node.waitStatus;
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
// 头结点的下一个节点
Node s = node.next;
if (s == null || s.waitStatus > 0) {
s = null;
// 这是个循环,每次处理前驱节点 t.prev,注意循环结束条件是 t != node,node 传进来的是头结点,所以这个循环会一直循环,直到头结点
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)
s = t; // 可能执行多次,但是最终是最接近头结点的下一个有效节点
}
if (s != null)
LockSupport.unpark(s.thread); // 唤醒线程
}
有两个需要注意的点:
-
传进来的节点状态改为 0
传进来的是头结点,头结点改为0,
release方法中if (h != null && h.waitStatus != 0)才会唤醒后面的节点,代码如下:Node h = head; if (h != null && h.waitStatus != 0) // unparkSuccessor 把头结点状态设置为 0,其他线程释放锁时这里不就条件不满足不会唤醒线程了吗 unparkSuccessor(h); // h.waitStatus = 0因为唤醒的线程会作为新的头结点,原来的头结点要出队(这里只是先把状态改为0,等后续唤醒的线程处理出队)
-
寻找唤醒的节点时是逆向的,从后往前遍历链表
为什么不从前往后遍历?因为 prev 指针比 next 指针更可靠,具体看下 enq 和 addWaiter 方法,先处理 prev 然后 cas 入队
头结点出队
这个方法入队时分析过,这里就不赘述了,做两件事:1 线程继续执行;2 当前节点作为新的虚拟节点(原来的节点等GC回收就出队了)
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)) {
// 当前节点作为新的虚拟节点(thread = null,prev = null,head = node),断开原来的头结点(出队)
setHead(node);
p.next = null;
failed = false;
return interrupted;
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt()) // 线程阻塞在这里,唤醒后从这里继续执行
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
共享模式加锁
同独占锁加锁一致,子类自己实现怎么加锁,加锁失败后的节点和线程处理交由 AQS
public final void acquireShared(int arg) {
if (tryAcquireShared(arg) < 0) // 加锁,子类实现。不管哪个子类实现都会遵循:如果加锁失败返回 -1
doAcquireShared(arg); // 加锁失败,入队和阻塞线程
}
入队和阻塞线程
private void doAcquireShared(int arg) {
// addWaiter 方法前面说过了,如果队列为空,初始化队列并入队;如果不为空直接入队,返回当前节点
final Node node = addWaiter(Node.SHARED); // 共享模式的节点类型是 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()) // park 线程
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
大致逻辑和独占模式一样,入队时如果当前节点就是同步队列中的第一个节点,本来应该入队后阻塞住,等释放锁后唤醒
但既然是第一个等待的节点,那么下一次就该它获取锁,如果这里能获取到就可以避免阻塞了
这毕竟是共享模式,和独占模式还是有一些区别,比如获取到锁有独占模式唤醒的是一个节点,共享模式就可能唤醒的不止一个
获取到锁后的处理是这个 setHeadAndPropagate() 方法完成的,跟踪一下源码:
private void setHeadAndPropagate(Node node, int propagate) { // node 是当前已获取到锁的线程
// 和独占模式一样,也是要把当前节点作为新的虚拟节点
Node h = head; // 这里记录一下旧的头结点
setHead(node);
/**
* 因为是共享锁,所以把等待共享锁的线程唤醒,如果是独占锁就没有这个处理。几个条件分析一下:
* 1)propagate > 0:当前线程获取到锁,要唤醒其他线程
* 2)h == null:
* 3)h.waitStatus < 0:比如是 -1,那么就要唤醒后继节点,但是这里没有使用 SINGAL 来判断而是判断的 <0
* 因为下面的方法 doReleaseShared() 会改节点状态为 -3
* 4)(h = head) == null || h.waitStatus < 0:重新取 head,防止 head 被更新导致当前的旧的 head 不准确
*/
// 和独占模式不一样的点:因为是共享锁,所以把等待共享锁的线程唤醒
if (propagate > 0 || h == null || h.waitStatus < 0 || (h = head) == null || h.waitStatus < 0) {
Node s = node.next;
if (s == null || s.isShared()) // 后继节点以共享模式等待才需要唤醒(可能唤醒多个),不然就是独占模式不用唤醒
doReleaseShared(); // 这个方法用于处理唤醒节点的(这里抢到锁了要唤醒,正常释放锁也要唤醒,放在释放锁中具体说)
}
}
共享模式释放锁
同前面的操作一致,释放锁也是由子类实现,这里看看释放成功后 AQS 怎么做的
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) { // 释放锁成功
doReleaseShared(); // 释放锁后要唤醒线程
return true;
}
return false;
}
出队和唤醒线程
unparkSuccessor() 方法用于唤醒线程,唤醒线程会更新头结点,因为在无线循环里,头结点可能不停更新(唤醒多个线程)直到把需要唤醒的节点全都唤醒
每唤醒一个头结点,新的节点作为新的头结点(虚拟节点),所以这里也是在不停地出队
private void doReleaseShared() {
for (;;) { // 又是无限循环
Node h = head;
if (h != null && h != tail) { // 队列不为空才有可以唤醒的线程
int ws = h.waitStatus;
if (ws == Node.SIGNAL) { // 头结点如果是 -1(waitStatus = -1 表示当前节点有责任唤醒后继节点)那就唤醒
// 修改头结点为0,为什么修改为0 前面说过了,这个头结点要出队,获取到线程的节点将作为新的头结点,这里是只是先改成 0
if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
continue;
unparkSuccessor(h); // 原来的头结点改为0后,唤醒线程,头结点会更新(头结点出队)
}
// 如果头结点是0,设置为 -3(比如 Semaphore 多个线程并发调用 release)
else if (ws == 0 && !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
continue; // loop on failed CAS
}
// 只要 unparkSuccessor() 执行了,并且成功唤醒节点(唤醒节点后会更新头结点),就继续循环,唤醒所有节点
// 如果 head 没变化,说明没有执行 unparkSuccessor() 或 unparkSuccessor() 没唤醒成功,就不用循环了
if (h == head)
break;
}
}
条件变量
等待
Condition.signal()、Condition.await() 的设计遵循 Monitor 模式(synchronized + Object.wait()/Object.notify())
所以条件变量只能独占模式使用,比如子类 ReentrantReadWritLock 的读锁是共享锁就不能使用条件变量
同 Object.wait()/Object.notofy() 一致:1 都要用在锁块中;2 顺序不能乱,都只能先阻塞再唤醒;2 阻塞时都会释放锁
public final void await() throws InterruptedException {
if (Thread.interrupted()) throw new InterruptedException();
// 节点入条件队列
Node node = addConditionWaiter();
// 释放锁(为什么叫完全释放,因为可能重入多次),释放后又会涉及
int savedState = fullyRelease(node);
int interruptMode = 0;
while (!isOnSyncQueue(node)) { // 节点是否已经转移到同步队列了
LockSupport.park(this); // 挂起线程
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
break;
}
// 尝试在同步队列中获取锁,acquireQueued 返回 true 表示线程被中断
// 如果中断模式不是 THROW_IE(抛出中断异常),则设置为 REINTERRUPT(重新中断)
if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
interruptMode = REINTERRUPT;
// 遍历条件队列,移除所有状态不是 CONDITION 状态的节点
if (node.nextWaiter != null) // clean up if cancelled
unlinkCancelledWaiters();
// 根据中断模式处理中断
if (interruptMode != 0)
reportInterruptAfterWait(interruptMode);
}
节点条件入队
/*
* 入队还是比较简单的
* 1)入同步队列未设置 waitStatus 的值,入条件队列时设置了的
* 2)同步队列是双向链表,头结点是虚拟节点,条件队列是单向链表,头结点不是虚拟节点
*/
private Node addConditionWaiter() {
Node t = lastWaiter;
if (t != null && t.waitStatus != Node.CONDITION) {
unlinkCancelledWaiters(); // 遍历条件队列,移除所有状态不是 CONDITION 状态的节点
t = lastWaiter;
}
Node node = new Node(Thread.currentThread(), Node.CONDITION); // 创建条件队列的节点,waitStatus = CONDITION
if (t == null) // 如果队列为空,和同步队列不一样:1)单向链表;2)没有虚拟节点
firstWaiter = node;
else
t.nextWaiter = node;
lastWaiter = node;
return node;
}
完全释放锁
final int fullyRelease(Node node) {
boolean failed = true;
try {
int savedState = getState(); // 获取当前锁的状态值(重入次数等)
// 尝试完全释放锁(包括所有重入次数)
if (release(savedState)) {
failed = false; // 释放成功,标记操作未失败
return savedState; // 返回释放前的状态值(用于后续恢复)
} else {
throw new IllegalMonitorStateException();
}
} finally {
if (failed)
node.waitStatus = Node.CANCELLED;
}
}
// 回到 release() 内部调用 unparkSuccessor(),还是熟悉的配方熟悉的味道(唤醒离头结点最近的有效节点,更新头结点)这里不再赘述
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
节点是否在同步队列判断
为什么需要这个判断?
因为可能其他节点 signal() 并发了,如果没有并发当前节点肯定是在条件队列
如果节点已经处于同步队列了,就不归 await() 处理了
final boolean isOnSyncQueue(Node node) {
/*
* node.waitStatus == Node.CONDITION:肯定还在条件队列
* node.prev == null:肯定不在同步队列(同步队列节点必须有前驱)
*/
if (node.waitStatus == Node.CONDITION || node.prev == null)
return false;
// node.next != null:肯定在同步队列
if (node.next != null) // If has successor, it must be on queue
return true;
/*
* 判断节点是否在同步队列上面两个条件不就够了吗,为什么还要通过 findNodeFromTail 再判断?
* 还真不够!条件队列节点转移到同步队列的步骤如下:
* 1)首先设置 node.prev 指向当前尾节点,node.prev != null
* 2)然后通过 CAS 将节点设为新的尾节点,compareAndSet(tail, oldTail, node)
* 3)最后设置原尾节点的 next 指针,node.next == null
* 如果步骤 3 还未完成,那么节点既不完全在条件队列,也不完全在同步队列
*/
return findNodeFromTail(node);
}
// 确定元素是否真的处于同步队列也简单,直接遍历同步队列,看队列中的节点有没有当前节点
private boolean findNodeFromTail(Node node) {
Node t = tail; // 从同步队列的尾节点开始向前遍历
for (;;) { // 无限循环,直到找到节点或遍历完整个队列
if (t == node) // 如果当前遍历的节点就是目标节点,说明在同步队列中
return true;
if (t == null) // 如果遍历到队列头部(null)还没找到,说明不在同步队列中
return false;
t = t.prev; // 移动到前一个节点继续查找
}
}
唤醒
public final void signal() {
// 因为设计遵循 wait()/notify() 所以非独占模式就报错
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
Node first = firstWaiter;
if (first != null)
doSignal(first); // 唤醒条件队列的头结点
}
// 源码是 do...while,翻译成 for(;;) 更直观,效果是一样的,这样便于理解
private void doSignal(Node first) { // first 要出队到同步队列,这是头结点,所以要把第二个节点作为新的头节点
for (;;) {
// 如果第二个节点为空,说明队列中只有一个头结点,出队后队列就空了(这也有赋值,firstWaiter 指向下一个节点)
if ((firstWaiter = first.nextWaiter) == null) {
lastWaiter = null; // 尾节点置为空
}
first.nextWaiter = null; // 头结点断开链接(此时节点就不属于条件队列的链表中了)
if (transferForSignal(first)) { // 转移节点到同步队列,如果转移成功就跳出循环
break;
}
// 如果转移不成功,就继续下一次循环,每次循环firstWaiter都指向下一个节点
// 直到firstWaiter为空了,表示没有没有更多节点可处理,也结束循环
if ((first = firstWaiter) == null) {
break;
}
}
}
// 条件队列转移到同步队列
final boolean transferForSignal(Node node) {
// 先把节点的 waitStatus 从 -2 改为 0(初始入同步队列时状态都是0)
if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
return false;
// 又见到 enq 了,节点队尾,返回前驱节点,然后把前驱节点改为 -1
Node p = enq(node);
int ws = p.waitStatus;
/*
* 如果前驱节点状态大于0,也就是1,说明前驱节点已取消,需要唤醒线程来处理
* 如果前驱节点改为 -1 失败,也需要唤醒线程来处理
*/
if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
LockSupport.unpark(node.thread);
return true;
}
为什么前驱节点已取消和前驱节点状态改为-1失败要唤线程?
首先要明白这两种情况都是异常的,这两中情况说明状态此时不是-1,不是-1意味着不会唤醒后继节点
唤醒线程就是用来处理异常的,这是调用了 singal() 阻塞的,所以唤醒时从 await() 方法中的 park() 处醒来
public final void await() throws InterruptedException {
...
while (!isOnSyncQueue(node)) {
LockSupport.park(this); // 从这里醒来
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
break;
}
// acquireQueued 方法中会处理取消的节点
if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
interruptMode = REINTERRUPT;
...
}
同步队列出入队流程
-
初始状态,首尾指针都是 null
flowchart LR head[head: null] tail[tail: null] -
线程A获取锁(直接成功)
队列无变化,首尾指针业务变化
flowchart LR head[head: null] tail[tail: null] -
线程B获取锁失败,入队
1)初始化虚拟节点,首尾指针先都指向虚拟节点
2)然后加入B节点,虚拟头结点和节点B组成链表
3)尾指针指向B节点flowchart LR H["H (thread=null)"] <--> B["B (thread=ThreadB)"] head --> H tail --> B -
线程C获取锁失败,入队
1)加入C节点,虚拟头结点、节点B、节点C组成链表
2)尾指针指向C节点flowchart LR H["H (thread=null)"] <--> B["B (thread=ThreadB)"] <--> C["C (thread=ThreadC)"] head --> H tail --> C -
线程A释放锁,唤醒线程B
1)线程B获取锁后,把自己设为头结点,并成为新的虚拟头结点(thread=null)
2)原来的虚拟头节点被舍弃flowchart LR B_new["B (thread=null)"] <--> C["C (thread=ThreadC)"] head --> B_new tail --> C -
线程B释放锁,唤醒线程C
1)线程C获取锁后也把自己设置新的虚拟头结点
2)再舍弃原来的虚拟头结点(原来的虚拟头结点是节点B)flowchart LR C_new["C (thread=null)"] head --> C_new tail --> C_new

浙公网安备 33010602011771号