JUC探险-9、Condition

本文详细探讨了Java并发工具包(JUC)中Condition接口的原理与实现,对比了Object类的wait/notify方法,阐述了Condition如何提供更丰富的线程等待/唤醒功能,包括响应中断、超时等待及条件唤醒等特性。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

一、:Condition

  ①Condition简介

    Condition是JUC提供的与Java的Object中wait/notify/notifyAll类似功能的一个接口,通过此接口,线程可以在某个特定的条件下等待/唤醒
    与wait/notify/notifyAll操作需要获得对象监视器类似,一个Condition实例与某个互斥锁绑定,在此Condition实例进行等待/唤醒操作的调用也需要获得互斥锁,线程被唤醒后需要再次获取到锁,否则将继续等待。
    而与原生的wait/notify/notifyAll等API不同的地方在于:JUC提供的Condition具有更丰富的功能,例如等待可以响应/不响应中断,可以设定超时时间或是等待到某个具体时间点。此外,一把互斥锁可以绑定多个Condition,这意味着在同一把互斥锁上竞争的线程可以在不同的条件下等待,唤醒时可以根据条件来唤醒线程,这是Object中的wait/notify/notifyAll不具备的机制。

    参照Object的wait和notify/notifyAll方法,Condition也提供了同样的方法:
      针对Object的wait方法:
        ●void await() throws InterruptedException:当前线程进入等待状态,如果其他线程调用condition的signal()或者signalAll()方法并且当前线程获取Lock,从await()方法返回。如果在等待状态中被中断会抛出被中断异常。
        ●long awaitNanos(long nanosTimeout):当前线程进入等待状态直到被通知、中断或者超时。
        ●boolean await(long time, TimeUnit unit)throws InterruptedException:同上一个,支持自定义时间单位。
        ●boolean awaitUntil(Date deadline) throws InterruptedException:当前线程进入等待状态直到被通知、中断或者到达超时时间。
      针对Object的notify/notifyAll方法:
        ●void signal():唤醒一个等待在condition上的线程,将该线程从等待队列中转移到同步队列中,如果在同步队列中能够竞争到Lock则可以从等待方法中返回。
        ●void signalAll():与上一个的区别在于能够唤醒所有等待在condition上的线程。

  ②代码分析

    1、主要流程

      JUC中Condition接口的主要实现类是AQS的内部类ConditionObject,它内部维护了一个队列,我们可以称之为等待队列,在某个Condition上等待的线程被signal/signalAll后,ConditionObject会将对应的节点转移到外部类AQS的同步队列中,线程需要获取到AQS同步队列的锁,才可以继续恢复执行后续的用户代码。

      await()方法的主要流程为:
        ●创建节点加入到等待队列。
        ●释放互斥锁。
        ●只要没有转移到同步队列就阻塞(等待其他线程调用signal/signalAll或是被中断)。
        ●重新获取互斥锁。

      signal()方法的主要流程为:
        ●将队列中第一个节点转移到同步队列。
        ●根据情况决定是否要唤醒对应线程。
        ●唤醒后从await()方法的循环中退出,开始重新获取互斥锁。

    2、对象创建

      通过lock.newCondition()方法创建condition对象,而这个方法实际上会new出一个ConditionObject对象。

      在锁机制的实现上,AQS内部维护了一个同步队列,如果是独占式锁的话,所有获取锁失败的线程的尾插入到同步队列。同样的,condition内部也是使用相似的方式,内部维护了一个等待队列,所有调用await()方法的线程会加入到等待队列中,并且线程状态转换为等待状态。
      通过属性字段可以看出,ConditionObject通过持有等待队列的头尾指针来管理等待队列。主要注意的是Node类复用了在AQS中的Node类,其节点状态和相关属性可以参考AQS中的相关说明。
      值得一提的是,与AQS的同步队列不同,ConditionObject持有的等待队列是一个单向队列

      可以多次调用lock.newCondition()方法创建多个condition对象,也就是一个lock可以持有多个等待队列。而在之前利用Object的方式实际上是指在对象Object对象监视器上只能拥有一个同步队列和一个等待队列,而并发包中的Lock拥有一个同步队列多个等待队列

    3、await()方法

      当调用condition.await()方法后会使得当前获取锁的线程进入到等待队列,如果该线程能够从await()方法返回的话一定是该线程获取了与condition相关联的锁。

public final void await() throws InterruptedException {
	// 响应中断
    if (Thread.interrupted())
        throw new InterruptedException();
    // 将当前线程包装成Node,尾插入到等待队列中
    Node node = addConditionWaiter();
    // 释放当前线程所持有的锁,在释放的过程中会唤醒同步队列中的下一个节点(如果没有持有锁,会抛出异常)
    int savedState = fullyRelease(node);
    int interruptMode = 0;
    // 检测此节点的线程是否在同步队列上。如果不在,则说明该线程还不具备竞争锁的资格,继续等待,直到检测到此节点在同步队列上
    // 在同步队列的三种可能情况:1、其它线程调用signal()将当前线程节点转移到同步队列并唤醒当前线程;2、其它线程调用signalAll;3、其它线程中断了当前线程,当前线程会自行尝试进入同步队列
    while (!isOnSyncQueue(node)) {
        LockSupport.park(this);
        // 如果已经中断了,则退出
        if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
            break;
    }
    // 重新获取互斥锁过程中如果中断并且interruptMode不为"抛出异常",将interruptMode设置为REINTERRUPT
    if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
        interruptMode = REINTERRUPT;
    // 清理等待队列中的不是在等待条件的节点
    if (node.nextWaiter != null) // clean up if cancelled
        unlinkCancelledWaiters();
    // 如果线程发生过中断则根据THROW_IE或是REINTERRUPT分别抛出异常或者重新中断
    if (interruptMode != 0)
        reportInterruptAfterWait(interruptMode);
}

      可以简单整理下代码流程:首先将当前线程新建一个节点同时加入到等待队列中,然后释放当前线程持有的锁。然后则是不断检测该节点代表的线程是否出现在同步队列中,如果不存在则一直挂起,否则参与竞争同步状态。

      ⅰ、addConditionWaiter()
private Node addConditionWaiter() {
	// 读取lastWaiter
    Node t = lastWaiter;
    // If lastWaiter is cancelled, clean out.
    // 如果等待队列中最后一个waiter节点状态不为CONDITION,则调用unlinkCancelledWaiters()清理队列
    if (t != null && t.waitStatus != Node.CONDITION) {
        unlinkCancelledWaiters();
        // 重读lastWaiter
        t = lastWaiter;
    }
    // 将当前线程包装成Node
    Node node = new Node(Thread.currentThread(), Node.CONDITION);
    // t如果为null, 初始化firstWaiter为当前节点
    if (t == null)
        firstWaiter = node;
    else
    	// 将队尾的next连接到node
        t.nextWaiter = node;
    lastWaiter = node;
    return node;
}

        方法作用主要是将当前线程加入到等待队列中。在加入到尾节点之前会清除所有状态不为CONDITION的节点。同时可以看出,等待队列是一个不带头节点的链式队列。

      ⅱ、fullyRelease()
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;
    }
}

        方法作用主要是释放该线程持有的锁。(同时唤醒后续线程)

      ⅲ、isOnSyncQueue()
final boolean isOnSyncQueue(Node node) {
	// 节点状态为CONDITION一定是在等待队列,或者前项节点为null也说明在等待队列(等待队列里节点是用nextWaiter来维护的,不用next和prev)
    if (node.waitStatus == Node.CONDITION || node.prev == null)
        return false;
    // 如果next不为null,一定是在同步队列的(同步队列在移出时,node会把next指向自己)
    if (node.next != null) // If has successor, it must be on queue
        return true;
    // 有可能node.prev的值不为null,但还没在队列中。(因为入队时CAS队列的tail可能失败)此时从tail向前遍历一次,确定node是否已经在同步队列上
    return findNodeFromTail(node);
}

// 从尾部向前遍历,判断节点是否在同步队列中
private boolean findNodeFromTail(Node node) {
    Node t = tail;
    for (;;) {
        if (t == node)
            return true;
        if (t == null)
            return false;
        t = t.prev;
    }
}

        方法作用主要是判断一个节点是否在同步队列上,是则返回true。

      ⅳ、checkInterruptWhileWaiting()
private int checkInterruptWhileWaiting(Node node) {
	// 线程未中断返回0
	// 线程中断且进入同步队列成功,返回THROW_IE,后续会抛出InterruptedException
	// 线程中断但未能进入同步队列,则返回REINTERRUPT,后续重新中断
    return Thread.interrupted() ?
        (transferAfterCancelledWait(node) ? THROW_IE : REINTERRUPT) :
        0;
}

// 进入同步队列
final boolean transferAfterCancelledWait(Node node) {
    if (compareAndSetWaitStatus(node, Node.CONDITION, 0)) {
        enq(node);
        return true;
    }
    // 进入同步队列失败,原因是signal()方法被调用,状态抢先更新了。此时自旋等待节点进入同步队列。
    while (!isOnSyncQueue(node))
        Thread.yield();
    return false;
}

        方法作用主要是检查是否阻塞,及阻塞后相关情况返回。

      ⅴ、unlinkCancelledWaiters()
private void unlinkCancelledWaiters() {
    Node t = firstWaiter;
    // 此变量记录上一个非CONDITION节点
    Node trail = null;
    while (t != null) {
        Node next = t.nextWaiter;
        if (t.waitStatus != Node.CONDITION) {
        	// 断开链接
            t.nextWaiter = null;
            if (trail == null)
            	// 如果trail为null,取当前节点的后继作为头节点的值(next可以为null)
                firstWaiter = next;
            else
            	// 如果trail不为null,把trail连接到当前节点的后继
                trail.nextWaiter = next;
            // 如果当前节点没有后继了,更新lastWaiter为trail
            if (next == null)
                lastWaiter = trail;
        }
        else
        	// 记录非CONDITION节点
            trail = t;
        t = next;
    }
}

        方法作用主要是清除所有状态不为CONDITION的节点。

      ⅵ、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();
}

        方法作用主要是判断阻塞类型,如果是THROW_IE则抛出InterruptedException,如果是REINTERRUPT则重新中断当前线程。

    4、signal()方法

      调用condition的signal()或者signalAll()方法可以将等待队列中等待时间最长的节点移动到同步队列中,使得该节点能够有机会获得锁。按照等待队列是先进先出(FIFO)的,所以等待队列的头节点必然会是等待时间最长的节点,也就是每次调用condition的signal()方法是将头节点移动到同步队列中。

public final void signal() {
	// 先检测当前线程是否已经获取锁
    if (!isHeldExclusively())
        throw new IllegalMonitorStateException();
    // 获取等待队列中第一个节点
    Node first = firstWaiter;
    if (first != null)
        doSignal(first);
}
      ⅰ、doSignal()
private void doSignal(Node first) {
    do {
    	// 移出头节点
        if ( (firstWaiter = first.nextWaiter) == null)
            lastWaiter = null;
        first.nextWaiter = null;
    // 如果转移到同步队列失败并且下一个节点不为null,则重试
    } while (!transferForSignal(first) && (first = firstWaiter) != null);
}

// 将节点转移到同步队列中
final boolean transferForSignal(Node node) {
    // 将该节点从状态CONDITION改变为初始状态0(如果失败,可能是状态已经变为CANCELLED)
    if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
        return false;
    // 将节点加入到同步队列中去,返回的是同步队列中node前面的一个节点
    Node p = enq(node);
    int ws = p.waitStatus;
    // 如果结点p的状态为CANCELLED或者修改waitStatus失败,则直接唤醒线程,以便其能开始尝试竞争锁
    if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
        LockSupport.unpark(node.thread);
    return true;
}

        方法主要做了两件事:1、把头节点移出等待队列。2、将移出的头节点放入同步队列。

      整体来看,signal()方法流程如下:
        ●判断当前线程是否已经获取了锁,如果没有获取则直接抛出异常,因为获取锁为唤醒的前置条件。
        ●如果线程已经获取了锁,则将唤醒等待队列的头节点。
        ●唤醒头节点时,先将等待队列中的头节点移出,然后调用AQS的enq()方法将其安全地移到同步队列中。
        ●最后判断如果该节点的同步状态为CANCELLED,或者修改状态为SIGNAL失败时,则直接调用LockSupport唤醒该节点的线程,以便其能开始尝试竞争锁。

    5、signalAll()方法

      此处大部分内容与signal()方法相似,直接来看区别的地方。

private void doSignalAll(Node first) {
	// 将firstWaiter和lastWaiter先置为null
    lastWaiter = firstWaiter = null;
    // 从first开始一直遍历到第一个null节点,对每个节点都做transferForSignal()操作
    do {
        Node next = first.nextWaiter;
        first.nextWaiter = null;
        transferForSignal(first);
        first = next;
    } while (first != null);
}

      可以看到此方法会从头节点开始遍历,从而实现唤醒所有的操作。

    6、总结

      直接来看一个完整的等待与唤醒的流程:
        ●await线程先通过lock.lock()方法获取锁成功后调用了await()方法进入等待队列
        ●另一个signal线程通过lock.lock()方法获取锁成功后调用了signal()或者signalAll()方法,使得await线程能够有机会移入到同步队列中。
        ●当其他线程释放锁后,await线程就有机会尝试获取锁,从而使得await线程能够从await()方法中退出,执行后续操作。如果await线程获取锁失败会直接进入到同步队列

系列文章传送门:

JUC探险-1、初识概貌
JUC探险-2、synchronized
JUC探险-3、volatile
JUC探险-4、final
JUC探险-5、原子类
JUC探险-6、Lock & AQS
JUC探险-7、ReentrantLock
JUC探险-8、ReentrantReadWriteLock
JUC探险-9、Condition
JUC探险-10、常见工具、数据结构
JUC探险-11、ConcurrentHashMap
JUC探险-12、CopyOnWriteArrayList
JUC探险-13、ConcurrentLinkedQueue
JUC探险-14、ConcurrentSkipListMap
JUC探险-15、BlockingQueue
JUC探险-16、ThreadLocal
JUC探险-17、线程池

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值