万字讲解AQS队列 , 从入门到精通, 你值得拥有

万字讲解AQS队列 , 从入门到精通, 你值得拥有

AQS (AbstractQueuedSynchronizer) 是 JUC 包下的一个核心… 它提供我们 实现自定义同步器的一个快捷模板

只要覆盖 一些方法 , 就能够自己写出一些同步器

 * <li> {@link #tryAcquire}
 * <li> {@link #tryRelease}
 * <li> {@link #tryAcquireShared}
 * <li> {@link #tryReleaseShared}
 * <li> {@link #isHeldExclusively}	//如果要使用等待队列..Condition , 那么首先 这个同步器 是要支持独占模式的, 还要重写这个方法
// 这些方法 .. 可以覆盖.  

从 大方向上来讲, AQS 底层使用了 Node 作为基础的存储单元, 其实现了一个 阻塞队列 和 一个 条件队列

因此让 AQS 具有了 2种模式, 共享模式 和 独占模式

通常情况下, AQS 只使用其中一种模式 来搭建同步器, 当然也可以同时使用2种模式

独占模式的含义是 : 同时间 仅仅只有 一个线程在工作 , 其他的线程会阻塞在这个同步器上

共享模式的含义是 : 一旦释放锁 , 阻塞的所有线程 都会开始工作 .


底层的Node节点

首先来看底层的具体存储…

static final class Node {
    // 该节点有2种存储模式
    static final Node SHARED = new Node();
    static final Node EXCLUSIVE = null;
    
    // 记录该节点的状态
    static final int CANCELLED =  1; 	// > 0 的情况, 是一种取消的了的状态 也就是说 只有在 <= 0 的情况下, 节点 才可能是正常工作的 (这个是表示自己的)
    static final int SIGNAL    = -1;  	// 表示需要信号 , 也就是需要被唤醒 (注意 : 在阻塞队列中, 节点上的状态, 通常表示的是下一个节点 需要的 情况)
    static final int CONDITION = -2;	// 表示需要等待一个条件
    static final int PROPAGATE = -3;	// 不常用... 表示可以传递而已

    volatile int waitStatus;	// 有上述的4个取值, 还有就是 0 , 表示没有状态 

    volatile Node prev;		//阻塞队列的前一个节点
    volatile Node next;		//阻塞队列的后一个节点
    volatile Thread thread;	//当前线程

    Node nextWaiter;	//条件队列中的下一个节点

   
    final boolean isShared() {		//判断节点是不是共享的
        return nextWaiter == SHARED;
    }
    
    final Node predecessor() throws NullPointerException {		//获得前一个节点
        Node p = prev;
        if (p == null)
            throw new NullPointerException();
        else
            return p;
    }

    Node() {    // Used to establish initial head or SHARED marker
    }

    Node(Thread thread, Node mode) {     // Used by addWaiter
        this.nextWaiter = mode;
        this.thread = thread;
    }

    Node(Thread thread, int waitStatus) { // Used by Condition
        this.waitStatus = waitStatus;
        this.thread = thread;
    }
}

小总结 :

  • 简单的说 : 底层使用的node节点类似一个双向链表, 但是多出了一个 nextWaiter 节点, 该节点记录了是哪一个线程 , 这个节点可以抽象的看成 就是当前线程.

AQS的重要成员属性

看过了底层的Node节点, 我们来看一看 作为同步器 有哪些成员变量

private transient volatile Node head;

private transient volatile Node tail;

private volatile int state;

十分简单… 就是保存了一个 头节点, 尾节点, 和一个状态… 这个状态 表示 ( 0 没有线程抢占, >0的情况 , 是同一个线程重入了多少次)


从独占模式开始学习

独占模式对应了2个方法

acquireacquireInterruptibly : 字面上也能看出来, 就是一个方法能够响应中断, 一个不能响应而已, 来看看如何演绎

请求成功Or失败排队逻辑
public final void acquire(int arg) {
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}

//这个方法是要求子类重写的
//因此 想要使用独占模式 (这个方法一定要重写)
protected boolean tryAcquire(int arg) {
    throw new UnsupportedOperationException();
}

//这是一个排队的逻辑处理
private Node addWaiter(Node mode) {		//模式 就just是 独占模式 , 这个参数 一般不会发生变化
    Node node = new Node(Thread.currentThread(), mode);	//封装成底层的Node节点
    
    //接下来是让node节点进行排队
    Node pred = tail;
    if (pred != null) {
        node.prev = pred;
        if (compareAndSetTail(pred, node)) {
            pred.next = node;
            return node; //先使用一次CAS , 如果排好了就正常返回
        }
    }		
    enq(node);		//如果第一次CAS 失败了, 就会进入 enq方法 或者 是tail 节点为null ,意味着没人使用这个同步器
    return node;
}

// 这个方法其实也很简单, 从它的名字来看,  enq , 就是 进入队列 的意思
// 内部使用的 就是 一个自旋的CAS的操作
private Node enq(final Node node) {
    for (;;) {
        Node t = tail;
        if (t == null) { // Must initialize
            if (compareAndSetHead(new Node()))
                tail = head;
        } else {
            node.prev = t;
            if (compareAndSetTail(t, node)) {
                t.next = node;
                return t;
            }
        }
    }
}

这是一个基本的请求锁的逻辑 一直到排队的逻辑 :

  1. 首先是尝试获得锁 (tryAcquire) 如果获得成功 那就结束了
  2. 如果获取失败, 就会让线程进入一个addWaiter的方法 , 进行一个排队
    • 这个方法首先会判断一下 tail 节点是不是为空
    • 如果 tail 节点不为空 就进行一次 cas , 看是否排队成功
    • 排队成功 就结束 排队逻辑, 如果排队不成功 或者 tail节点为空, 就会进入 enq方法
  3. 在enq 方法里, 它是一个 自旋的入队方法, 因此肯定是可以进入阻塞队列的 (这里要注意一下 enq的返回值是 当前节点的前一个节点 : 虽然在这个排队逻辑中 没有使用到 )

排队成功进入正式的阻塞环节

经过一个正常的排队逻辑之后, 该节点 (线程的抽象) 成功的进入了阻塞队列, 让我们接下来看 acquireQueued 方法 , 看看如何进行一个阻塞操作, 让进入阻塞队列的线程停下来

// 首先 我们要注意, 这个参数 node , 就是当前线程的抽象 node (arg 基本都是1)
final boolean acquireQueued(final Node node, int arg) {
    boolean failed = true;
    try {
        boolean interrupted = false;
        for (;;) {		//这个自旋 就是主要的逻辑
            final Node p = node.predecessor();	// p是 当前node 的前一个node
            //进入这个判断语句的条件 是 :
            // 1. 首先这个节点的前一个节点是 头节点
            // 2. 会再去尝试获得锁... (所以说这个方法很重要) (如果得到锁成功了 就进入了这个判断语句块中)
            if (p == head && tryAcquire(arg)) {  //这个自旋的唯一出口 , 就是进入这个循环 (当然还有就是发生了异常...)
                setHead(node);		// 将当前的节点 设置成为head节点.. 在这里我们也可以看出端倪. head节点应该表示的是正常运行的节点
                p.next = null; // help GC
                failed = false;	
                return interrupted;			// 这里是做了一个 中断的判断...需要查看下面的方法
            }
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {		//这个就相当于这个方法的安全网... 
        // 上面也说了.. 如果该自选的方法... 如果走正常出口, 那么failed 一定是 false ..
        // 也就是如果要调用这个方法.. 代价就是 抛出了异常
        if (failed)		
            cancelAcquire(node);		// 会让这个节点取消请求的过程
    }
}

这里先做出一个小补充, 关于 AQS 的 阻塞队列 :

  • 其实AQS的阻塞队列 是不包括 头部的 , 也就是 head 节点 不包括在内
  • 应该是从第二个节点开始, 才算是阻塞队列 (因为 head 节点在 独占模式的情形下, 是唯一正在run的 线程)

在这里插入图片描述


有了这个基础, 我们再回到 之前的方法中

  • 对于特殊位置的节点, 才有可能进入到 判断块中成功获得锁
  • 那么我们先来看 大多数的node 节点所要经过的方法 shouldParkAfterFailedAcquireparkAndCheckInterrupt
// 这个命名真的绝了... 在请求失败之后应该被挂起 ...
// 先来看参数, pred 是当前节点的前一个节点, node 是当前节点
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
    //这个方法 主要 就是在修改前一个节点的waitStatus ... 
    int ws = pred.waitStatus;
    if (ws == Node.SIGNAL)	// 前一个 是signal ... 其实就是表示已经进入过这个方法了 (而且成功设置了)
        return true;
    if (ws > 0) {		// 在之前的逻辑 是排队逻辑.. 这里是要开始阻塞线程的逻辑中
        do {			// 因此在做一个类似排队检查的过程中...发现 前面的节点 早就溜了... 那就可以把空缺补上
            //进入这里 就意味着 前面的节点提前取消了.... 可能是中断了. 所以 要把前置节点请出去
            node.prev = pred = pred.prev;
        } while (pred.waitStatus > 0);
        pred.next = node;
    } else {		// 修改 前面的节点
        compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
    }
    return false;		// 如果不是 前面一开始 不是 SIGNAL , 就返回 false , 让它自旋 再次进入判断
}

// 在 确保了前一个节点是 SIGNAL 的情况下 (进行挂起, 真正的停止)
private final boolean parkAndCheckInterrupt() {
    LockSupport.park(this);	//使用的是park方法		到这一步, 线程就停止了. 独占模式请求 锁的逻辑就结束了
    return Thread.interrupted();		// 这里是判断一下 从park状态出来 是不是因为中断了..
    // 如果是因为中断, 也就是意味着 前面的方法 全部都是true , 就会标记 interrupted = true
    // 这里值得注意的地方, 就是 就算是中断了, 也依然不会剔除 , 直到这个node 抢到了锁, 才会判断 , 会调用 selfInterrupt()
}

static void selfInterrupt() {	//因为之前调用了  interrupted() , 所以恢复一下中断状态
    Thread.currentThread().interrupt();
}


// LockSupport.park()
// 根据 源码的解释, 在线程进入park方法之后 
// 只有3种情况能够从park状态中出来
/*
     * <li>Some other thread invokes {@link #unpark unpark} with the
     * current thread as the target; or
     *
     * <li>Some other thread {@linkplain Thread#interrupt interrupts}
     * the current thread; or
     *
     * <li>The call spuriously (that is, for no reason) returns.
     * </ul>
     1. 其他的线程对这个线程 调用了 unpark方法
     2. 这个线程被中断了
     3. 发生了不明的原因...
*/
public static void park(Object blocker) {
    Thread t = Thread.currentThread();
    setBlocker(t, blocker);
    UNSAFE.park(false, 0L);
    setBlocker(t, null);
}

有一点大的小总结 :

  • 到这里为止, 基本上 acquire 的逻辑全部的捋清楚了
  • 在这个阶段里, 如果节点是在特殊的第二个位置, 那么会进行一次 tryAcquire 进行获得锁
    • 得锁成功 那就出去了, 正常执行逻辑
    • 如果失败了 , 就会进入一个 修改前节点 的逻辑
  • 在挂起前的状态修改操作中
    • 如果前节点已经被注明 是 SIGNAL 了, 那就返回true , ( 然后进入真正的阻塞逻辑 )
    • 如果前节点已经被取消了 , 那就全都 “请出去” ,直到一个不是 取消状态的 前节点 作为节点
    • 不然就 对前节点进行修改, 该成 SIGNAL 状态 (在后面2种情况下, 都会返回false 也就是会回到第一个步骤 进行自旋)
  • 进入逻辑阻塞的阶段, 使用LockSuppose 进行 park 操作 , 等待 从park 状态恢复过来
    • 出现下列3种情况的一种就会恢复过来 (1. unpark 2.中断 , 3.无原因 , (几乎不会出现))
    • 恢复过来之后, 第一件事情 就是查看一下是不是 由于中断, 由于中断的话. 就标记一下 interrupted = true , 依然进行自旋等待
    • 直到 该 node 抢到了锁
  • 如果已经是中断的线程, 那么在node 抢到锁的之后, 会对该线程 进行一个中断恢复 , 如果没有中断, 那就开始开始正常的执行

在这里插入图片描述

取消节点的过程 —

等等 等等 等等, 在开始下一个环节前, 我们来思考一下, 这个仅仅只是 acquire 方法, 它是一个不响应中断的设计

让我们简单的瞄一眼 响应中断的 acquireInterruptibly 方法

public final void acquireInterruptibly(int arg)
        throws InterruptedException {
    if (Thread.interrupted())	//如果已经中断了, 那就 抛出中断异常
        throw new InterruptedException();	
    if (!tryAcquire(arg))		//尝试获得锁, 失败之后 进入
        doAcquireInterruptibly(arg);
}
// 这个方法几乎和 acquireQueued 一模一样, 
// 唯一的区别 在于这方法 不返回 值了, 而且 不是标记中断状态, 而是直接抛出中断异常
private void doAcquireInterruptibly(int arg)
    throws InterruptedException {
    final Node node = addWaiter(Node.EXCLUSIVE);
    boolean failed = true;
    try {
        for (;;) {
            final Node p = node.predecessor();
            if (p == head && tryAcquire(arg)) {
                setHead(node);
                p.next = null; // help GC
                failed = false;
                return;
            }
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                throw new InterruptedException();
        }
    } finally {
        if (failed)	//那么进入这个方法的概率就比 acquiereQueued 高了
            cancelAcquire(node);		
    }
}

// -------- 新东西! 注意  . 取消这个节点 
private void cancelAcquire(Node node) {
    if (node == null)
        return;
    node.thread = null;
    
    Node pred = node.prev;
    while (pred.waitStatus > 0)		//清除前面也已经取消了的节点
        node.prev = pred = pred.prev;

    Node predNext = pred.next;

    node.waitStatus = Node.CANCELLED;	//将自己标记为已经取消的节点

    if (node == tail && compareAndSetTail(node, pred)) {
        // 如果当前为tail , 就交换一下 (当然在判断的时候, 可能有其他的node会跟上, 跟上的话 , 就会把这个节点请出去了..)那就会进入下一个分支 
        compareAndSetNext(pred, predNext, null);	//将 pred的下一个节点 修改成null . 全部清空
    } else {
        // 来到这个分支的情况分析 : 
        // 1. node != tail , 也就是可能是第二个位置的节点, 也可能是正常的节点 (不可能是head .. 因为head已经开始运行了)
        // 2. 之前 node 还是tail , 在判断的极端的时间内, tail被别人抢去了 (这种情况下, 其实node节点以及被后来者请出队列了)
        int ws;
        if (pred != head &&
            ((ws = pred.waitStatus) == Node.SIGNAL ||
             (ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) &&
            pred.thread != null) {
            // 进入这个分支, 首先 node的位置 并不是第二个, 其次前一个节点并没有取消
            Node next = node.next;
            // 做的事情就是把自己的后面的节点, 都交给前面一个节点
            // 让我们来思考一下 这个东西 真的真的正确吗
            // 先判断第一个情况, 并没有被后节点请出去, 这就意味着 后节点 已经阻塞住了 , 所以很自然的成功的
            // 第二种情况(极少发生) 已经被请出去了, 意味着 next 有可能为null , 但是不管有没有 这个cas 都不会成功了
            if (next != null && next.waitStatus <= 0)
                compareAndSetNext(pred, predNext, next);
        } else {
            // 进入这个分支, 也就是node 在那个特殊的位置, 那个可能得到锁的位置 , 这个时候, 就要唤醒一下后面的节点, 问问他能不能得到锁了 
            unparkSuccessor(node);
        }

        node.next = node; // help GC (把自己扔出去了)
    }
}

// 比较简单的一个方法, 就是唤醒 后一个 没取消的节点
private void unparkSuccessor(Node node) {
    int ws = node.waitStatus;
    if (ws < 0)
        compareAndSetWaitStatus(node, ws, 0);	//如果当前节点没有取消, 就把状态回归到0

    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;
    }
    if (s != null)
        LockSupport.unpark(s.thread);	//进行唤醒
}

小小小小总结 :

  • 不响应中断的node, 只有出现异常 才会进入 取消节点的环节, 响应中断的node , 中断之后 , 会进入 取消节点的环节
  • 如果取消的节点处在一个特殊的位置, 就会唤醒后方的节点, 让整个流程变的可用, 安全

也就是说, 如果不响应中断的话, 节点 基本上不会变成 -2 cancel 的状态


释放锁的环节

在上文中, 我们已经成功的把一个线程park住了…

而且也说过, 只有头节点 , 才可能在运行, 也就是 可能去做一个释放锁的逻辑

在 独占模式下的, 释放锁的逻辑, 对应的方式 是 release .

/**
 * 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}.
 *
 * @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}
 */
// 在注释中, 也已经表示了, 这是在独占模式下的释放锁的方式 , 可以用来实现 unlock
// 不过必须实现 tryRelease
public final boolean release(int arg) {
    if (tryRelease(arg)) {
        Node h = head;
        if (h != null && h.waitStatus != 0)	// 思考一下 ? 当什么情况下 h.waitStatus 不为0 ?
            // 答案很明了, 在上文中, 我们看清了阻塞的情况, 里面有一步就是修改前置节点的状态 , 让它变成 -1 SIGNAL 状态
            // 所以 waitStatus 不为 0 
            unparkSuccessor(h);		// 这个方法, 在上文也看过了, 就是唤醒 下一个节点 如果有的话
        return true;	//放锁成功
    }
    return false;	//释放锁失败
}

// 如果要使用独占方法的话, 一定要实现 tryRelease方法
protected boolean tryRelease(int arg) {
    throw new UnsupportedOperationException();
}

小总结 :

  • 释放锁的逻辑十分的简单… 只要通过 tryRelease 的逻辑判断, 就能正常的释放锁 , 唤醒下一个节点
  • 也就是说 tryRelease中 应该不光需要判断 有没有 release的资格, 而且还需要 正在的释放锁
    • 否则如果出现了 这边唤醒了 下一个节点…下个节点通过自旋 又得到锁失败了, 又陷入park状态…这就死锁了- -…


到这里为止! 我们已经讲清楚了 抢锁 和 释放锁 的逻辑. 让我们来看看 JUC 包下 , 实际是如何做到的

来一个 ReentrantLock 的源码放松一下

reentranLock , 在锁升级(JDK1.6)前 , 是一个 首选使用的锁, 性能又好 , 又能支持 synchronized 做不到的特性

比如说 可以中断 啊 , 能够多个条件等待啊

对于这个锁来说 , 里面有2个内置的 同步器, 一个是 公平锁, 一个是非公平锁 (默认) , 他们都是基于 Sync 类的, 而这个类 是基于 AQS的, 我们来看看它是如何实现的

类的内容有点冗长 , 我们就来看 它覆盖 aqs方法的部分

abstract static class Sync extends AbstractQueuedSynchronizer {
    private static final long serialVersionUID = -5179523762034025860L;

    // 新增了一个lock 的方法, 这个方法是用来 实现外层类的 lock接口的 lock方法的.  
    abstract void lock();

    // 在这个方法中, 并没有重写 tryAcquire 方法, 估计要留给后面的子类
    
    // 尝试释放
    protected final boolean tryRelease(int releases) {
        int c = getState() - releases;
        if (Thread.currentThread() != getExclusiveOwnerThread())
            throw new IllegalMonitorStateException();
        boolean free = false;
        if (c == 0) {		// c 表示如果释放锁之后  是 0 , 也就是真正的释放锁了
            free = true;	// 那就锁空闲了
            setExclusiveOwnerThread(null);	//没有独占
        }
        setState(c);	//重设 状态值
        return free;
    }
    
    protected final boolean isHeldExclusively() {	//得到当前独占的线程是否是当前线程
        return getExclusiveOwnerThread() == Thread.currentThread();
    }
}

这里先忽略掉了 Sync 的 条件队列相关的东西 , 再来看一下 公布锁 和 非公平锁 是怎么实现的

static final class FairSync extends Sync {
    private static final long serialVersionUID = -3000897897090466540L;

    // 正常抢锁
    final void lock() {
        acquire(1);
    }
    
    // 确实在这里 进行了 具体的实现 (对于 公平锁来说, 不存在提前抢锁)
    protected final boolean tryAcquire(int acquires) {
        final Thread current = Thread.currentThread();
        int c = getState();
        // 所以只有 当前没人抢锁, 且没有线程排队 , 本线程才回去抢锁
        if (c == 0) {
            if (!hasQueuedPredecessors() &&
                compareAndSetState(0, acquires)) {
                setExclusiveOwnerThread(current);
                return true;
            }
        }
        else if (current == getExclusiveOwnerThread()) {	// 不然就是本线程就是拿到锁了, 现在重入一次
            int nextc = c + acquires;
            if (nextc < 0)
                throw new Error("Maximum lock count exceeded");
            setState(nextc);
            return true;
        }
        return false;	//否则就是抢不到锁
    }
}


static final class NonfairSync extends Sync {
    private static final long serialVersionUID = 7316153563782823691L;

    // 正常抢锁前, 直接使用 cas 抢一下
    final void lock() {
        if (compareAndSetState(0, 1))
            setExclusiveOwnerThread(Thread.currentThread());
        else
            acquire(1);
    }

    protected final boolean tryAcquire(int acquires) {
        return nonfairTryAcquire(acquires);
    }
    
    // sync . nonfairTryAcquire
    final boolean nonfairTryAcquire(int acquires) {
        final Thread current = Thread.currentThread();
        int c = getState();
        if (c == 0) {	
            // 如果当前是空的, 直接使用cas 抢一下试试, 插个队, 插到就好了
            if (compareAndSetState(0, acquires)) {
                setExclusiveOwnerThread(current);
                return true;
            }
        }
        else if (current == getExclusiveOwnerThread()) {
            int nextc = c + acquires;
            if (nextc < 0) // overflow
                throw new Error("Maximum lock count exceeded");
            setState(nextc);
            return true;
        }
        return false;
    }
}

这个例子告诉我们… 只要看懂了 AQS , 实现一个自定义的锁, 感觉还是挺简单的

趁热, 我们开始下一个 话题 条件队列



条件队列

条件队列, 就是 node 需要等待某个事情发生, 才会进行

坐标位于 : AQS 这个类中的 public class ConditionObject

首先是 看它 的基本成员属性 (我觉得学习一个类的最后方法 就是先看它有啥属性 , 猜一猜也行)

// Node 依然是 使用的 AQS 的 Node 对象 , 同样记录了 一个头, 和一个尾 , 那么是双向链表 还是 单向链表呢? 这个留着先
private transient Node firstWaiter;
private transient Node lastWaiter;

/** Mode meaning to reinterrupt on exit from wait */
// 这边有2个特殊的含义 ... 这个标识意思是 退出了 wait状态 之后 要恢复中断
private static final int REINTERRUPT =  1;
/** Mode meaning to throw InterruptedException on exit from wait */
// 这个标识 要抛出中断异常
private static final int THROW_IE    = -1;

// 来看一个最简单的 加入 的方法...
private Node addConditionWaiter() {
    Node t = lastWaiter;
    
    if (t != null && t.waitStatus != Node.CONDITION) {	// 在这个条件队列里面, 如果 node 的状态 不是 Condition , 那么 就相当于取消了.  // 这就意味着, 在等待队列中, 只可能是condition
        unlinkCancelledWaiters();	
        t = lastWaiter;
    }
    Node node = new Node(Thread.currentThread(), Node.CONDITION);
    if (t == null)
        firstWaiter = node;
    else
        t.nextWaiter = node;	// 这里! 看清了, 使用的是Node 的 nextWaiter节点, 也就是说在 条件队列里面使用的是单项链表
    lastWaiter = node;
    return node;
}

// 这个方法 就是一个简单的全局扫描, 清理掉 所有 取消的等待节点
private void unlinkCancelledWaiters() {
    Node t = firstWaiter;
    Node trail = null;
    while (t != null) {
        Node next = t.nextWaiter;
        if (t.waitStatus != Node.CONDITION) {
            t.nextWaiter = null;
            if (trail == null)
                firstWaiter = next;
            else
                trail.nextWaiter = next;
            if (next == null)
                lastWaiter = trail;
        }
        else
            trail = t;
        t = next;
    }
}

小小总结 :

  • 通过对基础成员的分析, 我们得出了 条件队列 里面的 元素, 是呈现一种 单向链表 进行的
Condition.await() 方法

对于ConditionObject 来说, 有很多个await方法, 这些await 极大的丰富了…锁世界

具体的有

  • void await()
  • boolean await(long time, TimeUnit unit) : 对于这个方法来说 , 返回值如果为true , 那就是在规定的时间内抢到了锁, 如果为false , 就是在之后抢到的锁
  • long awaitNanos(long nanosTimeout) : 这个和 上面的方法几乎相同, 纳秒级单位 : nb (返回值 负数 就是超时了, 正数 表示还多出了多少纳秒)
  • void awaitUninterruptibly() (就这个不会响应中断)
  • boolean awaitUntil(Date deadline) (绝对日期 , 到这个时间之前…) 返回值 同 await

和上文同样的道理, 看完 基础的成员变量之后, 我们来看一下 public 的方法… (一个类 我们只能使用它的public方法, 相当于一个类的入口)

// await() 方法

首先我们可以来看await方法… 这个命名 应该是用来区分 object.wait()的方法

它 也就是 来模拟 object.wait() 方法 的

我们想一想, synchronize 也需要一个 monitor 来作为监视器, 这样才可以调用 monitor的wait方法

同理. 如果 要使用 Condition . await 方法, 那么必然也需要先获得锁

我们这里来思考一个问题 ? 为什么 在条件队列里面, 好像所有的操作, 都没有使用cas , 或者 是加锁什么的? 会不会遇到并发问题呢?

答案是否定的 … DougLea大神 早就想到了这个问题, 或者根本不存在这个问题, 因为 要对条件队列进行操作, 必须是获得锁的…没有锁 直接报错了 . 所以对于 条件队列 而言, 永远都是单线程在操作它. 所以不存在并发的问题

/**
 * Implements interruptible condition wait.
 * <ol>
 * <li> If current thread is interrupted, throw InterruptedException.
 * <li> Save lock state returned by {@link #getState}.
 * <li> Invoke {@link #release} with saved state as argument,
 *      throwing IllegalMonitorStateException if it fails.
 * <li> Block until signalled or interrupted.
 * <li> Reacquire by invoking specialized version of
 *      {@link #acquire} with saved state as argument.
 * <li> If interrupted while blocked in step 4, throw InterruptedException.
 * </ol>
 + ++ ++++++++++++++++++++++++++++ 这边对这一段进行一个翻译
 大概就是
  ----- 如果当前线程已经被中断了, 那就抛出中断异常
  ----- 保存await之前的状态 (为了以后的恢复 (而且想要await 必须持有锁, 那就意味着现在的状态 就是线程当前的状态))
  ----- 调用 release 来保存状态 , 如果失败, 抛出异常
  ----- 之后就阻塞线程, 直到 被signalled 或者 中断
  ----- 重新调用acquire 来获得释放锁之前的状态
  ----- 如果在第四步已经中断了, 那就抛出中断异常
  
 ++++++++++++++++++
 基本说完了如何实现的逻辑... 我们来看一看如何具体实现的吧
 */
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)) {				//判断当前node 是不是在 阻塞队列中 不在就进入语句块
        LockSupport.park(this);		// 其实不在阻塞队列 就意味着 还没唤醒, 这个时候就简单的挂起当前线程
        
        //醒来之后看看是不是因为中断. 这个方法里面的逻辑 一会说
        if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
            break;
    }
    
    //到这里, 能够确保的是 该node 已经进入了 阻塞队列... 那么就去进行一个抢锁
    // 注意(acquireQueued的返回值 不是抢到了锁, 而是否是中断)
    if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
        interruptMode = REINTERRUPT;	//如果是中断的... (可能是执行方法之后中断的, 也可能是signal之后中断的)
    // 不管这样的中断, 我都设置成 REINTERRUPT , (这个标识 signal 之后中断的)
    
    // 全局清理已经取消的其他等待者
    if (node.nextWaiter != null) // clean up if cancelled
        unlinkCancelledWaiters();
   
    // 如果是因为中断, 那就根据需求, 要么抛出中断异常, 要么恢复中断
    if (interruptMode != 0)
        reportInterruptAfterWait(interruptMode);	//其实只有在signalled 之前 , 才会抛出异常
}

// 这个方法 是在 aqs 里面的 (就是字面意思, 释放所有的锁 (如果这个锁可以重入的话))
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;
    }
}

// aqs.isOnSyncQueue
final boolean isOnSyncQueue(Node node) {
    if (node.waitStatus == Node.CONDITION || node.prev == null)	// 如果是等待的..或者是在head的位置..那么肯定不在阻塞队列
        return false;
    if (node.next != null) // If has successor, it must be on queue ,如果有后继者...那一定是在阻塞队列里面了
        return true;
    
    return findNodeFromTail(node);	//全局寻找, 从后往前
}

//aqs.findNodeFromTail
private boolean findNodeFromTail(Node node) {
    Node t = tail;
    for (;;) { // 做一个全局的寻找
        if (t == node)
            return true;
        if (t == null)
            return false;
        t = t.prev;
    }
}

到这里为止, 基本知道了一下 await 的具体逻辑, 不得不说 这是个很复杂的方法…(感觉)

  • 首先一个前提, 获得锁之后 才能够调用await方法. (意味着能够调用await方法, 该node 是处在head的位置)
  • 加入条件队列中, (提前判断一下是不是中断了)
  • 调用fullyRelease去存储信息 以及 释放锁 (这个方法其实是依赖 tryRelease的, 所以依然需要重写tryRelease方法)
  • 之后就进入park状态 (等待从park状态中恢复过来)
    • 我觉得从park状态恢复过来之后的一些列操作 是比较复杂的
  • 从park状态恢复过来, 第一件事情 就是判断一下是不是因为中断 导致从park状态恢复
    • 如果是的话, 再看一下是在 得到了signal信号之前 还是之后
      • 如果是之前的话, (这个情况下 需要手动把自己的node 放入到阻塞队列中) (标记需要抛出异常)
      • 如果是之后的话, 也就是这个线程应该从await状态恢复过来, 但是还没有完成搬运的情况, 那么等搬运完成 (标记恢复中断)
  • 进行一个正常的抢锁环节, 抢到之后, 判断是不是中断了… 以及一个清理条件队列里面其他取消的node
  • 如果中断的话 (根据 signal 前, 和signal 后)
    • signal前 是抛出中断异常, 这是因为让应用程序去响应 await处的中断
    • signal之后, 这就意味着 是 await 之后的代码出现了 中断, 这就恢复中断, 让具体的代码抛出中断异常 就完事了

来简单的看一下 遗漏的那个方法.

// conditionObject 下的方法
private int checkInterruptWhileWaiting(Node node) {
    return Thread.interrupted() ?
        (transferAfterCancelledWait(node) ? THROW_IE : REINTERRUPT) :
        0;
}

// aqs 下的方法
final boolean transferAfterCancelledWait(Node node) {
    if (compareAndSetWaitStatus(node, Node.CONDITION, 0)) {
        enq(node);
        return true;
    }
    // 这个表示是在 signal之后的... (之后让其他的代码去响应这次中断)
    while (!isOnSyncQueue(node))
        Thread.yield();		
    return false;
}

//conditionObject 下的方法
private void reportInterruptAfterWait(int interruptMode)
    throws InterruptedException {
    if (interruptMode == THROW_IE)
        throw new InterruptedException();
    else if (interruptMode == REINTERRUPT)
        selfInterrupt();	// 说明在逻辑上 await方法 已经被激活了, 那就让应用程序之后的代码 响应这次中断信号
}

在这里插入图片描述


Condition.signal()方法

有2个方法

  • void signal() (对应object.notify)
  • void signalAll() 对应 notifyall

// 如果看上去十分的简单

public final void signal() {
    if (!isHeldExclusively())	// 先判断一下是不是获得锁了.  (这里我思考了一下 , 为什么await方法里, 并没有开头这样的东西) 而是 需要在 tryAcquire 方法中 自己去主动的判断一下是不是 获得了锁 ? 这个问题 不晓得...
        throw new IllegalMonitorStateException();	
    Node first = firstWaiter;
    // 如果这个 条件队列里面是有线程在等待的, 那就进行唤醒操作 . 也就是 singal
    if (first != null)
        doSignal(first);
}

private void doSignal(Node first) {
    do {
        // 对头节点进行一个释放 , 如果释放之后, 下一个是null , 意味着这是最后一个节点 , 让tail  = null
        if ( (firstWaiter = first.nextWaiter) == null)
            lastWaiter = null;
        first.nextWaiter = null;	// 帮助gc , 让 first 游离
        
        //如果唤醒失败的话, 就进行一个持续的自旋, 直到空为止
    } while (!transferForSignal(first) && (first = firstWaiter) != null);
}

// 尝试进行唤醒
final boolean transferForSignal(Node node) {
    // 当前节点的状态 居然不是Condition? 那就说明取消了呀!
    if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
        return false; // 失败

    Node p = enq(node);	//不然就把它加入阻塞队列. 注意了 : enq 的返回值 是加入队列的前一个节点
    int ws = p.waitStatus;
    if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
        // 如果前一个节点 是取消状态的, 或者 cas的时候 发生失败 ?
        // 什么时候会发生失败? 在判断的时候 ws 进行了取消? 应该是吧
        // 那就对node的节点进行unpark ( 注意 : 这里是 其他的线程 对 node的线程进行的unpark)
        // node 节点 还停留在 await方法中呢!!!!!!!!!!!!!!!!!! 这个很重要 (否则老乱了)
        LockSupport.unpark(node.thread);
    
    // 表示唤醒成功了!
    return true;
}

小总结 :

  • 这个唤醒的逻辑确实不是很复杂
  • 首先判断一下是不是得到锁了之后进行的singal调用
  • 在条件队列中选择出一个不是取消状态的node , 把它放进 阻塞队列, 然后如果它的前一个node 是取消状态的…那就唤醒这个线程

这边注意一个问题哦 : enq 方法中 并没有把 cancel 状态的node 请出 队列

acquireQueued() 方法里面 的 shouldParkAfterFailedAcquire() 方法 才会把取消的节点请出队列

这也是如果 node 前面是 已经取消的节点, 那么一定要把 node 唤醒, 让它进行acquireQueued方法把 取消的节点进行请出

还有一个 signalAll 方法 就是遍历一下 条件队列的所有节点, 调用 transferForSignal(Node node) 方法

到这里… 我们基本知道了条件队列的使用 , 以及 条件队列 和 阻塞队列 如何转换



共享模式

最后一个是共享模式 , 也是AQS 里的 最后一块内容了

使用的底层存储和之前一样的, Node节点 , 话不多说, 直接来看 AQS 对 共享模式的 public 方法

请求共享逻辑, 以及排队原理, 和得到锁之后进行唤醒后置节点
// aqs的方法 ...
// 这个方法看上去十分简约... 首先进行一下获取共享 arg , 如果小于0 (估计是没获取到) , 那就调用 doAcquireShared方法
public final void acquireShared(int arg) {
    if (tryAcquireShared(arg) < 0)
        doAcquireShared(arg);
}

// 这个方法 需要在子类中 自行实现, 也就是如果需要使用共享模式, 那么需要重写这个方法
// 关于这个方法的返回值是什么 . 我们可以看一下 源代码的注解
/**
 @return a negative value on failure; zero if acquisition in shared
     *         mode succeeded but no subsequent shared-mode acquire can
     *         succeed; and a positive value if acquisition in shared
     *         mode succeeded and subsequent shared-mode acquires might
     *         also succeed, in which case a subsequent waiting thread
     *         must check availability. (Support for three different
     *         return values enables this method to be used in contexts
     *         where acquires only sometimes act exclusively.)  Upon
     *         success, this object has been acquired.
     
     大概意思就是 , 如果返回 <0   那么就是失败的
     如果 = 0 , 那么之后 再次请求 就不会成功
     如果 > 0  那么之后再次请求 还是可能成功的

**/
protected int tryAcquireShared(int arg) {
    throw new UnsupportedOperationException();
}

//如果失败的话, 就进入 doAcquireShared
// 吐槽一下...这个代码格式 都快刻进DNA里了.. 这和 独占模式 中的获取 ....几乎一模一样
private void doAcquireShared(int arg) {
    final Node node = addWaiter(Node.SHARED);
    boolean failed = true;
    try {
        boolean interrupted = false;
        for (;;) {
            final Node p = node.predecessor();
            if (p == head) {	// 独占模式下...这里 和下面的判断是放一起的
                // 独占模式 (if (p == head && tryAcquire().....))
                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())	// 这边还是一样的设计, 会陷入一个阻塞状态, 会把前一个节点设为SIGNAL
                interrupted = true;
        }
    } finally {
        if (failed)	// 同样的安全网的措施, 如果整个代码 产生了异常, 就会把这个节点进行排除
            cancelAcquire(node);
    }
}

小停顿:

  • 在这里, 已经基本的展示了 共享模式的请求方式… 看上去 几乎 和 独享模式的请求方式一模一样…
  • 几乎同样的逻辑处理… 这里就不说了… 把这些方法进行展开. 看看细节
// 让我们来看看 这个参数, node 毫无疑问 是当前的 node 节点, propagate 是 tryAcquireShared 的返回值 , 而且是大于 0 的
private void setHeadAndPropagate(Node node, int propagate) {
    Node h = head; // Record old head for check below
    setHead(node);
    
    // 这个判断就比较奇怪了.. 我追溯了一下 AQS 内部 对这个方法的调用, 调用之前都是判断propagate...这参数需要 > 0 , 那这样这个判断基本每次都进入....
    if (propagate > 0 || h == null || h.waitStatus < 0 ||
        (h = head) == null || h.waitStatus < 0) {
        Node s = node.next;	 // 得到下一个节点...
        if (s == null || s.isShared())	// 下一个节点为空 或者 是共享的 
            doReleaseShared();	// 疑点 ?为啥为null 也要调用这个方法.. 看看这个方法是这么进行的
    }
}

private void doReleaseShared() {
    // 对于这个方法, 我们能看出来, 它是一个自旋的设计, 而且出口只有一个, 那就是 head 没有发生变化的时候
    // 首先来分析一下 head发生变化是什么情况. 我们看代码 中间 有个 unparkSuccessor的操作
    // 根据上面方法进来 来看, 当前head 其实就是 当前的node . 也就是 只有在unpark 后面的节点的时候, head才有可能发生改变
    for (;;) {
        Node h = head;
        if (h != null && h != tail) {
            int ws = h.waitStatus;
            if (ws == Node.SIGNAL) {	// 什么时候 当前的 状态会是 SIGNAL 呢? 很简单, 后面一个节点需要唤醒的时候
                // 因为该节点是 共享模式的, 那就意味着 不会阻塞后面的节点...
                if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))	// 交换失败 只有一种情况, 就是下面的unparkSuccessor 的另外的线程 进行了... 出现几率很小. 问题不大
                    continue;            // loop to recheck cases
                unparkSuccessor(h);		// 唤醒h的下一个节点
            }
            // 怎么才能够进入这个分支呢? 假设一个情况. 2个线程同时进入这个循环中, 在上一个判断的时候, 一个线程快一点点, 进行了一次cas 把 h 的状态变成了 0 , 那么后一个线程只能进入到这个 地方, 把 h 的状态 设置成 可传播的, 这样如果还有同时进入的线程, 就会什么都不做 离开这个方法...
            else if (ws == 0 &&	
                     !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))	// 把该点设置为可传播的
                continue;                // loop on failed CAS
        }
        if (h == head)                   //有没有被修改..那么就赶紧撤离.
            break;
    }
}

总结 :

  • 到这里 , 基本看完了共享模式下的 请求 锁的方式
  • 依然是一个插队的设计, 先去 tryAcquireShared 一下, 看看能不能直接抢到, 如果能直接抢到, 那么就ok
  • 抢不到的话, 进入一个排队的逻辑中去, 排完队之后, 依然去看一下 是不是在特殊位置(阻塞队列的第一个) 看看能不能抢一下锁
  • 如果还是抢不到, 那只能陷入 park 状态了, 等待有缘线程 进行唤醒 (或者中断 )
    • 中断的话 , 做的是一样的操作, 等到拿到锁之后 , 会进行中断恢复
    • 正常被唤醒的话, 就会 会到 第2步 , 是一个自旋的操作
  • 如果获得了锁 , 就会把当前head 设置成自己, 由于它是一个 共享模式 的节点, 所以 它还会去唤醒 在它后面排队的节点


主动进行共享模式的释放过程

对应方法, releaseShared , 这个方法 依赖于 tryReleaseShared , 所以 想要使用共享模式, 那就需要 覆盖 tryAcquireShared方法, 和 releaseShared方法

public final boolean releaseShared(int arg) {
    if (tryReleaseShared(arg)) {
        doReleaseShared();		//这个方法我们也看过了... 就是释放能够释放锁的节点
        return true;
    }
    return false;
}

大总结 :

  • 这个过程十分的简单. 到这里为止, AQS的几乎所有的模块 都已经看完了, 其余的方法 都是一些简单的方法, 主要用途是用来监控AQS的一些方法.
  • AQS 有2个大模式 : 独占模式, 和共享模式, 通常情况下 只使用一种 模式, 当然也可以两种模式一起使用
  • 在AQS中 有2个重要的队列, 一个是阻塞队列 , 一个是条件队列.
    • 在独占模式下 , 阻塞队列中 的所有 node 都将会串行化的执行 , 也就是同一时间 , 只会出现一个节点进行 实际的代码运行
    • 在共享模式下 , 阻塞队列中的head节点 会对后面的节点进行唤醒
    • 对于 条件队列而言 , 进入 条件队列的前提 是要在 head的位置, 这个时候 加入到条件队列里面进行释放锁的操作, park线程
    • 等待 这个条件的发生, 让别的线程把这个node节点转移到 阻塞队列之中 , 对线程进行唤醒
    • 对于node来说, 从 条件队列中 移动到 阻塞队列, 就意味着 条件以及发生, 成功的进入了 等待 轮到本node 就可以运行了
  • …感觉AQS 也就这么一回事…emmm 细节实现是真的强. 勉强能看懂.

如果有人能看到这里… 感谢一波, 顺便点个攒也行

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值