JAVA并发-AQS条件队列Condition

659 阅读15分钟

这是我参与8月更文挑战的第7天,活动详情查看:8月更文挑战

一、Condition简介

Condition 是一个多线程协调通信的工具类里面提供了await/signal方法,可以让某些线程一起等待某个条件(condition),只有满足条件时,线程才会被唤醒。

在Java并发编程中提供了Condition接口,此接口的await/signal机制是设计用来代替监视器锁的wait/notify机制的。通过Condition接口可以实现更加复杂、更加细粒度的多线程间的协调。

Condition接口代码如下:

public interface Condition {
    /**
    * 线程等待
    */
    void await() throws InterruptedException;
    /**
    * 线程等待指定纳秒数
    */
    void awaitUninterruptibly();
    /**
    * 线程等待指定时间
    */
    long awaitNanos(long nanosTimeout) throws InterruptedException;
    /**
    * 线程等待,不响应中断
    */
    boolean await(long time, TimeUnit unit) throws InterruptedException;
    /**
    * 线程等待至指定的日期
    */
    boolean awaitUntil(Date deadline) throws InterruptedException;
    /**
    * 线程唤醒
    */
    void signal();
    /**
    * 唤所有的阻塞线程
    */
    void signalAll();
}

Condition接口和Object类等待/唤醒方法对比:

Object类的方法Condition接口备注
void wait()void await()都有类似的方法
void wait(long timeout)long awitNanos(long nanosTimeout)时间单位和返回值不同
void wait(long timeout,int nanos)boolean await(long time,TimeUnit unit)时间单位,参数和返回值不同
void notify()void signal()都有类似的方法
void notifyAll()void signalAll()都有类似的方法
void awitUninterruptibly()Condition接口独有
boolean awaitUntil(Date deadline)Condition接口独有

Object的监视器方法与Condition接口对比:

对比项Object监视器方法Condition
前置条件获取对象的锁调用Lock.lock()获取锁 调用Lock.newCondition()获取Condition对象
调用方式直接调用 如:object.wait()直接调用 如:condition.wait()
等待队列个数一个多个
当前线程释放锁并进入等待状态支持支持
当前线程释放锁并进入等待状态,在等待状态中不响应中断不支持支持
当前线程释放锁并进入超时等待状态支持支持
当前线程释放锁并进入等待状态到将来的某个时间不支持支持
唤醒等待队列中的一个线程支持支持
唤醒等待队列中的全部线程支持支持

二、Condition的实现

1、ConditionObject 内部类

AQS(AbstractQueuedSynchronizer)的内部类ConditionObject实现了Condition接口。

final class ConditionObject implements Condition {

        /**
         * 条件队列的头节点
         */
        private transient Node firstWaiter;
        /**
         * 条件队列的尾节点
         */
        private transient Node lastWaiter;

        /**
         * ConditionObject 默认的构造函数
         */
        public ConditionObject() {
        }
}

结构如图(单向队列):

31.png

2、Await()方法

  • await()方法过程相当于同步队列的首节点(获取了锁的节点)移动到Condition的等待队列中
 public 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);//如果不在同步队列中 那就阻塞当前线程 等待唤醒
                /*
                 * 能执行到下面的代码 说明线程从阻塞状态中唤醒了 唤醒可能有2种情况
                 * 1:是线程发生了中断
                 * 2:是线程接受到signal信号 从阻塞状态中被唤醒
                 * checkInterruptWhileWaiting 返回值有3个
                 * 0表示:线程没有被中断
                 * 1 REINTERRUPT表示:中断在signal之后发生的
                 * -1 THROW_IE表示:中断在signal之前发生的
                 */
                if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)//检查是否发生过线程中断 0表示没有发生
                    break;// 如果线程没有中断 说明被signal唤醒  那就继续判断是否唤醒了当前线程 如果是当前线程 会进入到同步队列中
            }
            /*
             * 这边的代码 就是当前的node已经在Sync Queue 中了
             * acquireQueued 我们在之前独占锁加锁的时候 也分析过  就是去获取资源 获取不到的话 就排队等待继续阻塞
             * acquireQueued返回true 说明在进入Sync队列中 等待的过程中锁的过程中也发生了中断 
             *acquireQueued返回true 返回false 说明没有发送过中断 那下面的赋值就不会走到 
             *如果acquireQueued返回true 而且interruptMode是非THROW_IE 那个整个方法就是REINTERRUPT的结果 因为都不需要抛出异常
             */
            if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
                interruptMode = REINTERRUPT;//记录线程中断的表示位
            /*
             *这边的意思就是如果当前节点nextWaiter不是等于null的说明 node节点还是和Condition
             * queue 关联着的  那就执行一下清理操作 吧condition queue里面的非等待节点剔除
             * 那种情况下会走到这步呢 那就是当前的interruptMode是THROW_IE的时候
             * 为什么呢 因为THROW_IE的意思 是中断发送在signal之前 signal
             * 因为如果是signal的话 当前节点的nextWaiter为被置为null的  可以回看下代码
             */
            if (node.nextWaiter != null)
                unlinkCancelledWaiters();
            if (interruptMode != 0)//这边0 说明一直没发生过中断
                reportInterruptAfterWait(interruptMode);
        }
  • addConditionWaiter()这个方法的主要作用是把当前线程封装成 Node,添加到 Condition 中的等待队列。这里的队列不再是双向链表,而是单向链表。
         /**
         * 新增一个新的等待节点到等待的条件队列中
         *
         * @return its new wait node
         */
        private Node addConditionWaiter() {
            Node t = lastWaiter;//等待条件的队列的最后一个
            //如果最后的lastwaiter 节点状态是非Condition 说明已经取消 就清理ConditionQueue的方法
            if (t != null && t.waitStatus != Node.CONDITION) {
                unlinkCancelledWaiters();
                t = lastWaiter;//unlinkCancelledWaiters方法里面lastWaiter可能又重写赋值了
            }
            Node node = new Node(Thread.currentThread(), Node.CONDITION);//当前线程包装成node节点
            /*
             * t是null 说明尾节点为null 说明条件队列中没有值 所以node 成了firstWaiter
             * t不为null 那就加入到队尾
             * */
            if (t == null)
                firstWaiter = node;
            else
                t.nextWaiter = node;
            lastWaiter = node;//lastWaiter 重写赋值 因为node是最后加入的 node就是lastWaiter
            return node;
        }

执行完addConditionWaiter这个方法之后,就会产生一个这样的condition队列 32.png

  • unlinkCancelledWaiters()向等待队列中的LastWaiter加入节点时,LastWaiter不是CONDITION状态,从头遍历等待队列中移除取消等待的节点。
         /**
         * 条件队列从头部开始 移除非CONDITION节点
         */
        private void unlinkCancelledWaiters() {
            Node t = firstWaiter;//头节点赋值给t
            Node trail = null;//trail是t的next节点的上一个为CONDITION的节点
            //这个循环做的是从头节点开始移除不是CONDITION的节点
            while (t != null) {
                Node next = t.nextWaiter;//next为t的下一个节点
                if (t.waitStatus != Node.CONDITION) {//如果t的状态不是CONDITION 说明不应该在条件队列中 取消了 要移除
                    t.nextWaiter = null;//把t的下一个节点设置为null 这样让t 和整个条件队列链表断开 也方便GC
                    /**
                     *trail 是null 说明是第一次进来吧 但是第一次的t是firstWaiter 这个时候firstWaiter的节点为CONDITION
                     * 所以下面有个赋值把 firstWaiter的下一个节点 赋值给firstWaiter 意思就是说 让下个节点成为头节点
                     * 如果trail不是为null 那就把当地节点的next赋值给trail的下个节点  因为当前节点t 不可用了 所以要将t的
                     * 下个节点  重新和链表关联起来 也就是说重新指向上一个节点 而trail其所就是t的上一个有效的节点
                     * 所以有了这个赋值
                     * */
                    if (trail == null)
                        firstWaiter = next;
                    else
                        trail.nextWaiter = next;
                    /*
                     * 如果next 等于null 了 说明t没有下个节点了  这个时候trail 应该就是有效的最后一个节点
                     * */
                    if (next == null)
                        lastWaiter = trail;
                } else
                    trail = t;//trail相当于一个临时的变量  这边的赋值就是我上面说的   trail是next的上一个有效的节点值
                t = next;//next赋值给t 准备下一次的循环
            }
        }
  • fullyRelease()方法释放同步状态并且唤醒在同步队列中头结点的后继节点引用的线程,如果释放成功则正常返回,否则抛出异常。
/**
     * 释放当前节点持有的所有资源,并且唤醒同步队列中的head节点去获取资源
     */
    final int fullyRelease(Node node) {
        boolean failed = true;//表示 是否释放失败
        try {
            int savedState = getState();//获取同步器的状态值state
            if (release(savedState)) {//就是释放资源 唤醒等待的线程去获取资源 之前已经描述过 不清楚的 看下第二篇文章
                failed = false;
                return savedState;
            } else {
                throw new IllegalMonitorStateException();//释放失败 抛出异常
            }
        } finally {
            if (failed)
                node.waitStatus = Node.CANCELLED;//如果释放失败 就把当前节点设置去取消  着就解释了 为什么之前加入节点的时候回去做检查
            // 丢弃非Condition的节点
        }
    }

此时,同步队列会触发锁的释放和重新竞争。ThreadB 获得了锁。

31.png

  • isOnSyncQueue()方法 就是判断当前节点是否在同步队列SyncQueue中,如果是的话 就跳出while循环执行后面的方法,如果不在的话 那就要进入while循环体。
    /**
     * 判断当前node 是否在同步队列中
     */
    final boolean isOnSyncQueue(Node node) {
        /*
         *节点的状态是condition一定不再同步队列中 
         *如果节点加入到同步队列中 使用enq方法 那么当前节点的pre 一定是非空的 
         *那么如果当前pre是为null
         *那就不在Sync queue 中
         */
        if (node.waitStatus == Node.CONDITION || node.prev == null)
            return false;
        if (node.next != null) // 如果当前节点有后继节点 必然是在同步队列中的 因为next是同步队列中的node 才会存在这一的情况
            return true;
        return findNodeFromTail(node);//去同步队列中匹配node 节点
    }
    
    /**
     * 从尾部节点开始搜索 看是否能找到当前的node节点
     */
    private boolean findNodeFromTail(Node node) {
        Node t = tail;//同步队列的尾部节点
        for (; ; ) {
            if (t == node)// t ==node 说明在同步队列能找到 返回true
                return true;
            /*
             * t==null 第一次循环说明tail节点不存在 说明同步队列就是不存在的 那node更不可能存在于同步队列中返回false
             * 后面的循环t 就是之前的节点的前pre节点  如果为null 说明已经找到了头部节点了 都没有匹配到node 也返回false
             */
            if (t == null)
                return false;
            t = t.prev;
        }
    }
  • checkInterruptWhileWaiting()方法用于线程被唤醒之后判断在等待期间是否被中断。
  • 如果当前线程被中断,则调用 transferAfterCancelledWait() 方法判断后续的处理应该是 抛出InterruptedException还是重新中断。
    /**
     * Mode meaning to reinterrupt on exit from wait
     */
    private static final int REINTERRUPT = 1;
    /**
     * Mode meaning to throw InterruptedException on exit from wait
     */
    private static final int THROW_IE = -1;

    /**
     * 检查是否发生过线程中断
     * 返回0表示:线程没有被中断
     * 1表示:中断在signal之后发生的
     * -1表示:中断在signal之前发生的
     */
    private int checkInterruptWhileWaiting(Node node) {
        return Thread.interrupted() ?
                (transferAfterCancelledWait(node) ? THROW_IE : REINTERRUPT) :
                0;
    }
    /**
     * Transfers node, if necessary, to sync queue after a cancelled wait.
     * Returns true if thread was cancelled before being signalled.
     */
    final boolean transferAfterCancelledWait(Node node) {
        /*
         * 这个地方给大家特别说明下:
         * 刚才上面我提到过 被唤醒有2中方式 可能是被signalled 或者被interrupted
         * 下面的有个CAS的操作 就是 将当前节点的状态更新成0
         * 如果更新成功说明了 当前节点的状态依旧是CONDITION 也就是说还在条件队列中 那就说明了不是被signal唤醒的 那就是被中断了
         * 同理 如果更新失败 则说明当前节点的状态 已经被修改了  那说明就是被signalled了的 因为被signal 会将当前节点状态修改 转移到Sync queue中
         */
        if (compareAndSetWaitStatus(node, Node.CONDITION, 0)) {
            enq(node);//这边更新成功 说明当前线程发生了中断 而且中断在signal之前 这边做一个补偿操作 把节点放入到Sync 队列中
            return true;
        }
        /*
         * 这边又判断了下 当前节点是否在同步队列中  为什么还要判断呢 是因为虽然发生了signal
         * 但是 我们看下transferForSignal的方法能知道  是先执行修改节点状态的CAS操作 然后再执行enq的入队操作
         * 所以这边虽然状态已经修改 但是可能线程正在执行enq 方法  所以这边判断了下 如果没有在Sync队列中
         * 那当前线程就坐下yield 就是线程执行让出一下 意思就是稍等会儿
         */
        while (!isOnSyncQueue(node))
            Thread.yield();
        return false;
    }
  • reportInterruptAfterWait()方法,此时的interruptMode等于THROW_IE,即发生了中断。当interruptMode等于THROW_IE时,reportInterruptAfterWait()方法将抛出中断异常。
private void reportInterruptAfterWait(int interruptMode)
    throws InterruptedException {
    // 直接抛出异常
    if (interruptMode == THROW_IE)
        throw new InterruptedException();
    // 重新中断,线程自己处理
    else if (interruptMode == REINTERRUPT)
        selfInterrupt();
}

static void selfInterrupt() {
     Thread.currentThread().interrupt();
}

await()方法示意:

  1. 将当前线程加入到条件等待队列
  2. 释放当前线程获取的同步状态,唤醒后继节点
  3. 挂起当前线程
  4. 唤醒之后(检查是否被中断),重新获取同步状态 111.png

3、Signal()/SignalAll()方法

  • signal()方法 是从Contidion头部开始选一个合法的节点 转换到SyncQueue中。
   public void signal() {
       if (!isHeldExclusively())
          throw new IllegalMonitorStateException();
       Node first = firstWaiter;
       if (first != null)
          doSignal(first);
    }
  • isHeldExclusively()方法需要子类重写,其目的在于判断当前线程是否为获取锁的线程。
 /**
     * 一个判断 判断是否用于锁的线程和释放线程是同一个  子类从写实现
     */
    protected boolean isHeldExclusively() {
        throw new UnsupportedOperationException();
    }
  • doSignal()将头节点移出等待队列。
/**
     * 使得条件队列中的第一个没有被cancel的节点 enq到同步队列的尾部
     */
    private void doSignal(Node first) {
        do {
            /*
             * 这边说明条件队列只有first 一个节点转移完first节点设置lastWaiter也为null
             * 设置first的nextWaiter 等于null
             */
            if ((firstWaiter = first.nextWaiter) == null)
                lastWaiter = null;
            first.nextWaiter = null;//first 要被加入到同步队列中 修改nextWaiter==null
        } while (!transferForSignal(first) &&
                (first = firstWaiter) != null);
    }
  • transferForSignal()方法将节点添加到同步队列中。
 /**
     * 将node节点从调节队列中转换到同步队列中  如果返回是true 那说明转换成功
     */
    final boolean transferForSignal(Node node) {
        /*
         * 如果当前的CAS操作失败 说明node节点的状态已经不是condition了 可能已经被cancel了 所以返回false
         */
        if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
            return false;

        Node p = enq(node);//将当前的node节点 加入到同步队列中 独占锁的时候已经分析过  返回的节点p是node节点的prev节点
        int ws = p.waitStatus;
        /*
         * 这边ws是node的prev 节点p的状态  如果p的ws 大于0 那说明p已经cancel了  那就可以直接唤醒node节点
         * 这边不明白的可以去结合shouldParkAfterFailedAcquire 方法看下 这个方法里面有如果node的pre节点是Cancel的话 会做重写寻找pre节点
         * 同样的下面的CAS 操作将node的前驱节点P的ws状态修改为signal失败  说明当前的p节点的状态已经被别的线程修改了
         * 那就要去唤醒node节点线程去获取资源锁
         * 之前我们独占锁的时候都说过  同步队列中 节点都是通过自己的前驱节点去唤醒的
         */
        if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
            LockSupport.unpark(node.thread);
        return true;
    }
  • signalAll()方法 是将所有ConditiaonQueue中node节点转换到SyncQueue中。
        public void signalAll() {
            if (!isHeldExclusively())
                throw new IllegalMonitorStateException();
            Node first = firstWaiter;
            if (first != null)
                doSignalAll(first);
        }
        
         /**
         * 移除条件队列中所有节点 挨个转移到同步队列中
         */
        private void doSignalAll(Node first) {
            lastWaiter = firstWaiter = null;//因为所以节点 都已经转移 所以条件队列就为null 了
            do {
                Node next = first.nextWaiter;
                first.nextWaiter = null;
                transferForSignal(first);
                first = next;
            } while (first != null);//循环转移 直到最后一个nextWaiter等于null
        }
        
  • awaitNanos()方法具有超时功能, 与响应中断的功能, 不管中断还是超时都会 将节点从 Condition Queue 转移到 Sync Queue。
/**
 * Impelemnts timed condition wait
 *
 * <li>
 *     If current thread is interrupted, throw InterruptedException
 *     Save lock state returned by {@link #getState()}
 *     Invoke {@link #release(int)} with saved state as argument,
 *     throwing IllegalMonitorStateException if it fails
 *     Block until aignalled, interrupted, or timed out
 *     Reacquire by invoking specified version of
 *     {@link #acquire(int)} with saved state as argument
 *     If interrupted while blocked in step 4, throw InterruptedException
 * </li>
 */
/**
 * 所有 awaitXX 方法其实就是
 *  0. 将当前的线程封装成 Node 加入到 Condition 里面
 *  1. 丢到当前线程所拥有的 独占锁,
 *  2. 等待 其他获取 独占锁的线程的唤醒, 唤醒从 Condition Queue 到 Sync Queue 里面, 进而获取 独占锁
 *  3. 最后获取 lock 之后, 在根据线程唤醒的方式(signal/interrupt) 进行处理
 *  4. 最后还是需要调用 lock./unlock 进行释放锁
 */
@Override
public final long awaitNanos(long nanosTimeout) throws InterruptedException {
    if(Thread.interrupted()){                                   // 1. 判断线程是否中断
        throw new InterruptedException();
    }
    Node node = addConditionWaiter();                           // 2. 将线程封装成一个 Node 放到 Condition Queue 里面, 其中可能有些清理工作
    int savedState = fullyRelease(node);                       // 3. 释放当前线程所获取的所有的锁 (PS: 调用 await 方法时, 当前线程是必须已经获取了独占的锁)
    final long deadline = System.nanoTime() + nanosTimeout;   // 4. 计算 wait 的截止时间
    int interruptMode = 0;
    while(!isOnSyncQueue(node)){                              // 5. 判断当前线程是否在 Sync Queue 里面(这里 Node 从 Condtion Queue 里面转移到 Sync Queue 里面有两种可能 (1) 其他线程调用 signal 进行转移 (2) 当前线程被中断而进行Node的转移(就在checkInterruptWhileWaiting里面进行转移))
        if(nanosTimeout <= 0L){                               // 6. 等待时间超时(这里的 nanosTimeout 是有可能 < 0),
            transferAfterCancelledWait(node);                 //  7. 调用 transferAfterCancelledWait 将 Node 从 Condition 转移到 Sync Queue 里面
            break;
        }
        if(nanosTimeout >= spinForTimeoutThreshold){      // 8. 当剩余时间 < spinForTimeoutThreshold, 其实函数 spin 比用 LockSupport.parkNanos 更高效
            LockSupport.parkNanos(this, nanosTimeout);       // 9. 进行线程的 block
        }
        if((interruptMode = checkInterruptWhileWaiting(node)) != 0){   // 10. 判断此次线程的唤醒是否因为线程被中断, 若是被中断, 则会在checkInterruptWhileWaiting的transferAfterCancelledWait 进行节点的转移; 返回值 interruptMode != 0
            break;                                                     // 说明此是通过线程中断的方式进行唤醒, 并且已经进行了 node 的转移, 转移到 Sync Queue 里面
        }
        nanosTimeout = deadline - System.nanoTime();                    // 11. 计算剩余时间
    }

    if(acquireQueued(node, savedState) && interruptMode != THROW_IE){ // 12. 调用 acquireQueued在 Sync Queue 里面进行 独占锁的获取, 返回值表明在获取的过程中有没有被中断过
        interruptMode = REINTERRUPT;
    }
    if(node.nextWaiter != null){                                    // 13. 通过 "node.nextWaiter != null" 判断 线程的唤醒是中断还是 signal, 因为通过中断唤醒的话, 此刻代表线程的 Node 在 Condition Queue 与 Sync Queue 里面都会存在
        unlinkCancelledWaiters();                                      // 14. 进行 cancelled 节点的清除
    }
    if(interruptMode != 0){                                           // 15. "interruptMode != 0" 代表通过中断的方式唤醒线程
        reportInterruptAfterWait(interruptMode);                      // 16. 根据 interruptMode 的类型决定是抛出异常, 还是自己再中断一下
    }
    return deadline - System.nanoTime();                            // 17 这个返回值代表是 通过 signal 还是 超时
}

4、Condition的显著特点

  • 1、同时依赖两个同步等待队列,一个是AQS提供,另一个是ConditionObject提供的。
  • 2、await()方法会释放AQS同步等待队列中的阻塞节点,这些节点会加入到条件等待队列中进行阻塞。
  • 3、signal()或者signalAll()会把条件等待队列中的节点重新加入AQS同步等待队列中,并不解除正常节点的阻塞状态。
  • 4、接第3步,这些进入到AQS同步等待队列的节点会重新竞争成为头节点,接下来的步骤其实也就是前面分析过的独占模式下的AQS的运作原理。

三、利用Condition实现生产者消费者模式

import java.util.LinkedList;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class BoundedQueue {

    private LinkedList<Object> buffer;    //生产者容器
    private int maxSize ;           //容器最大值是多少
    private Lock lock;
    private Condition fullCondition;
    private Condition notFullCondition;
    BoundedQueue(int maxSize){
        this.maxSize = maxSize;
        buffer = new LinkedList<Object>();
        lock = new ReentrantLock();
        fullCondition = lock.newCondition();
        notFullCondition = lock.newCondition();
    }

    /**
     * 生产者
     * @param obj
     * @throws InterruptedException
     */
    public void put(Object obj) throws InterruptedException {
        lock.lock();    //获取锁
        try {
            while (maxSize == buffer.size()){
                notFullCondition.await();       //满了,添加的线程进入等待状态
            }
            buffer.add(obj);
            fullCondition.signal(); //通知
        } finally {
            lock.unlock();
        }
    }

    /**
     * 消费者
     * @return
     * @throws InterruptedException
     */
    public Object get() throws InterruptedException {
        Object obj;
        lock.lock();
        try {
            while (buffer.size() == 0){ //队列中没有数据了 线程进入等待状态
                fullCondition.await();
            }
            obj = buffer.poll();
            notFullCondition.signal(); //通知
        } finally {
            lock.unlock();
        }
        return obj;
    }

}

往期推荐