AQS 原理

AQS(AbstractQueuedSynchronizer)

JUC 的基石

  1. 提供加锁释放锁的模板方法,子类根据自身自定义实现,比如公平/非公平、独占/共享 等
  2. 共性的操作自己内部已经实现好了,不需要子类再实现,比如线程入队、唤醒、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;
  
		...
}

同步队列出入队流程

  1. 初始状态,首尾指针都是 null

    flowchart LR head[head: null] tail[tail: null]
  2. 线程A获取锁(直接成功)

    队列无变化,首尾指针业务变化

    flowchart LR head[head: null] tail[tail: null]
  3. 线程B获取锁失败,入队

    1)初始化虚拟节点,首尾指针先都指向虚拟节点
    2)然后加入B节点,虚拟头结点和节点B组成链表
    3)尾指针指向B节点

    flowchart LR H["H (thread=null)"] <--> B["B (thread=ThreadB)"] head --> H tail --> B
  4. 线程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
  5. 线程A释放锁,唤醒线程B

    1)线程B获取锁后,把自己设为头结点,并成为新的虚拟头结点(thread=null)
    2)原来的虚拟头节点被舍弃

    flowchart LR B_new["B (thread=null)"] <--> C["C (thread=ThreadC)"] head --> B_new tail --> C
  6. 线程B释放锁,唤醒线程C

    1)线程C获取锁后也把自己设置新的虚拟头结点
    2)再舍弃原来的虚拟头结点(原来的虚拟头结点是节点B)

    flowchart LR C_new["C (thread=null)"] head --> C_new tail --> C_new
posted @ 2023-05-24 15:57  CyrusHuang  阅读(34)  评论(0)    收藏  举报