【Java并发编程】Condition详解

一、简介

1.1 什么是Condition

任意一个Java对象,都拥有一组监视器方法(定义在java.lang.Object上),主要包括 wait()、wait(long timeout)、notify() 以及 notifyAll() 方法,这些方法与 synchronized 同步关键字配合,可以实现等待/通知模式。Condition接口也提供了类似Object的监视器方法,与Lock配合可以实现等待/通知模式,但是这两者在使用方式以及功能特性上还是有差别的。——摘自《Java并发编程的艺术》

下面是Condition与Object的监视器方法的对比(摘自《Java并发编程的艺术》):

1.2 Condition接口

我们来看一下Condition接口的定义:

public interface Condition {

    //等待,当前线程接受到信号前、或中断前一直处于等待状态
    void await() throws InterruptedException;

    //等待,当前线程接受到信号前一直处于等待状态,不响应中断
    void awaitUninterruptibly();

    //等待,当前线程接受到信号前、或中断前、或到达指定等待时间之前一直处于等待状态,返回值 = nanosTimeout - 实际消耗的时间,返回值 <= 0表示超时
    long awaitNanos(long nanosTimeout) throws InterruptedException;

    //等待,当前线程接受到信号前、或中断前、或到达指定等待时间之前一直处于等待状态,返回boolean类型,表示是否在指定时间内获取到接受信号,false表示超时。
    //它与上一个方法不同在于可以自定义超时时间单位,等同于awaitNanos(unit.toNanos(time)) > 0
    boolean await(long time, TimeUnit unit) throws InterruptedException;

    //等待,当前线程在接收到信号前、被中断或到达指定最后唤醒一个等待线程。如果所有的线程都在等待此条件,则选择其中的一个唤醒。在从 await 返回之前,该线程必须重新获取锁。期限之前一直处于等待状态
    boolean awaitUntil(Date deadline) throws InterruptedException;

    //唤醒一个等待线程。如果所有的线程都在等待此条件,则选择其中的一个唤醒。在从 await 返回之前,该线程必须重新获取与Condition相关的锁。
    void signal();

    //唤醒所有等待线程。如果所有的线程都在等待此条件,则唤醒所有线程。在从 await 返回之前,每个线程都必须重新获取与Condition相关的锁。
    void signalAll();
}

        Condition 是一种广义上的条件队列。他为线程提供了一种更为灵活的等待/通知模式,线程在调用 await() 方法后执行挂起操作,直到线程等待的某个条件为真时才会被唤醒。Condition 必须要配合锁一起使用,因为对共享状态变量的访问发生在多线程环境下。一个 Condition 的实例必须与一个Lock绑定,因此Condition一般都是作为Lock的内部实现

二、基本使用

以基本的B线程唤醒A线程为例:

2.1 Object#wait/notify 等待唤醒写法

public static void main(String[] args) throws InterruptedException {
    //对象锁
    Object obj=new Object();

    //创建线程A
    Thread threadA = new Thread(()->{
        System.out.println("A尝试获取锁...");
        synchronized (obj){
            System.out.println("A获取锁成功!");
            try {
                TimeUnit.SECONDS.sleep(1);
                System.out.println("A开始释放锁,并开始等待...");
                //线程A开始等待
                obj.wait();
                System.out.println("A被通知继续运行,直至结束。");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

    });
    //创建线程B
    Thread threadB = new Thread(()->{
        System.out.println("B尝试获取锁...");
        synchronized (obj){
            System.out.println("B获取锁成功!");
            try {
                TimeUnit.SECONDS.sleep(3);
                //线程B开始唤醒线程A
                obj.notify();
                System.out.println("B随机通知lock对象的等待队列中某个线程!");
                System.out.println("B执行完毕,开始释放锁...");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    });
    //启动线程A
    threadA.start();
    //这里为了是线程A先执行,避免B先执行了notify导致A永远无法被唤醒
    TimeUnit.SECONDS.sleep(1);
    //启动线程B
    threadB.start();
}

//执行结果
A尝试获取锁...
A获取锁成功!
A开始释放锁,并开始等待...
B尝试获取锁...
B获取锁成功!
B随机通知lock对象的等待队列中某个线程!
B执行完毕,开始释放锁...
A被通知继续运行,直至结束。

2.2 Condition#await/signal 等待唤醒写法

public static void main(String[] args) throws InterruptedException {
    //创建lock对象
    Lock lock = new ReentrantLock();
    //新建condition
    Condition condition = lock.newCondition();

    //创建线程A
    Thread threadA = new Thread(()->{
        System.out.println("A尝试获取锁...");
        lock.lock();
        try {
            System.out.println("A获取锁成功!");
            TimeUnit.SECONDS.sleep(1);
            System.out.println("A开始释放锁,并开始等待...");
            //线程A开始等待
            condition.await();
            System.out.println("A被通知继续运行...");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }finally {
            lock.unlock();
            System.out.println("A线程释放了锁,执行结束!");
        }

    });
    //创建线程B
    Thread threadB = new Thread(()->{
        System.out.println("B尝试获取锁...");
        lock.lock();
        try {
            System.out.println("B获取锁成功!");
            TimeUnit.SECONDS.sleep(3);
            //线程B开始唤醒线程A
            condition.signal();
            System.out.println("B随机通知lock对象的等待队列中某个线程!");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }finally {
            lock.unlock();
            System.out.println("B线程释放了锁,执行结束!");
        }
    });
    //启动线程A
    threadA.start();
    //这里为了是线程A先执行,避免B先执行了notify导致A永远无法被唤醒
    TimeUnit.SECONDS.sleep(1);
    //启动线程B
    threadB.start();
}

//执行结果
A尝试获取锁...
A获取锁成功!
A开始释放锁,并开始等待...
B尝试获取锁...
B获取锁成功!
B随机通知lock对象的等待队列中某个线程!
B线程释放了锁,执行结束!
A被通知继续运行...
A线程释放了锁,执行结束!

三、源码解析

3.1 ConditionObject类

ConditionObject 是 Condition 在java并发中的具体的实现,由于Condition的操作需要获取相关的锁,而AQS则是同步锁的实现基础,所以ConditionObject则定义为AQS的内部类。每个Condition对象都对应一个等待队列。ConditionObject定义如下:

// AbstractQueuedSynchronizer
public class ConditionObject implements Condition, java.io.Serializable {
    //序列化版本号
    private static final long serialVersionUID = 1173984872572414699L;
    
    //条件(等待)队列的第一个节点
    private transient Node firstWaiter;
    
    //条件(等待)队列的最后一个节点
    private transient Node lastWaiter;
    
    //。。。。。。
}

3.1.1 等待队列

每个 ConditionObject 都包含一个FIFO队列,队列中的节点类型是AQS的内部类——Node类,每个节点包含着一个线程引用,该线程就是在该Condition对象上等待的线程。与CLH同步队列不同的是,Condition的等待队列是单向队列,即每个节点仅包含指向下一节点的引用,如下图所示:

3.1.2 ConditionObject#await() 方法解析

调用 Condition#await() 方法会使当前线程进入等待状态,同时会加入到Condition等待队列同时释放锁。await() 方法代码如下:

// AbstractQueuedSynchronizer#ConditionObject
public final void await() throws InterruptedException {
    //响应中断
    if (Thread.interrupted())
        throw new InterruptedException();
    //将当前线程加入到等待队列中---------(1)
    Node node = addConditionWaiter();
    //释放当前线程持有的锁锁资源,不管当前线程重入多少次,全部置0---------(2)
    int savedState = fullyRelease(node);
    //中断模式
    int interruptMode = 0;
    //判断节点是否在同步队列(SyncQueue)中,即是否被唤醒---------(3)
    while (!isOnSyncQueue(node)) {
        //如果不在同步队列,则在JVM级别挂起当前线程
        LockSupport.park(this);
        //线程被唤醒后,检查是否发生了中断---------(4)
        //	被唤醒有两种原因:1是发生了中断,2是别的线程调用了signal
        if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
            break;  //如果发现线程已经中断了,则直接退出循环
    }
    //被唤醒后执行自旋操作尝试获取锁,同时判断线程是否被中断
    if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
        //获取锁成功,则标记线程需要重新设置中断标记位
        interruptMode = REINTERRUPT; 
    //取消后进行清理
    if (node.nextWaiter != null) // clean up if cancelled
        //清理等待队列中不为CONDITION状态的节点
        unlinkCancelledWaiters();---------(5)
    if (interruptMode != 0)
        //根据中断模式处理中断---------(6)
        reportInterruptAfterWait(interruptMode);
}

从如上代码观察中,不难发现,await() 主要做了四件事:

  1. 调用 addConditionWaiter() 方法构建新的节点封装线程信息并将其加入等待队列。
  2. 调用 fullyRelease(node) 释放锁资源(不管此时持有锁的线程重入多少次都一律将state置0),同时唤醒同步队列中后继节点的线程。
  3. 调用 isOnSyncQueue(node) 判断节点是否存在同步队列中,在这里是一个自旋操作,如果同步队列中不存在当前节点则直接在JVM级别挂起当前线程。
  4. 当前节点线程被唤醒后,即节点从等待队列转入同步队列时,则调用 acquireQueued(node, savedState) 方法执行自旋操作尝试重新获取锁资源。

注释标记了6个重点方法,分别来看看:

(1)addConditionWaiter() 方法:将线程封装为节点,加入到等待队列

如果没有中断异常抛出,则调用 addConditionWaiter() 方法将当前线程加入到等待队列中,方法如下:

// AbstractQueuedSynchronizer#ConditionObject
private Node addConditionWaiter() {
    //获取尾结点
    Node t = lastWaiter;
    //如果t不为空且其等待状态不为CONDITION,表示该节点不处于等待状态,需要清除掉
    if (t != null && t.waitStatus != Node.CONDITION) {
        //清理等待队列中不为CONDITION状态的节点
        unlinkCancelledWaiters();
        //重新获取新的尾结点
        t = lastWaiter;
    }
    //创建包含当前线程的结点,状态为CONDITION
    Node node = new Node(Thread.currentThread(), Node.CONDITION);
    //如果t为空,则表示队列为空,因此新节点为第一个节点
    if (t == null)
        //设置头结点
        firstWaiter = node;
    //否则队列不为空,将节点插入到最后一个位置
    else
        //节点插入到最后一个位置
        t.nextWaiter = node;
    //设置尾结点
    lastWaiter = node;
    //返回节点
    return node;
}

//清除条件队列中所有状态不为Condition的节点
private void unlinkCancelledWaiters() {
    //获取头结点
    Node t = firstWaiter;
    Node trail = null;
    //从头往尾清除掉等待状态不为CONDITION的节点
    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;
    }
}

(2)fullyRelease() 方法:完全释放锁

当前节点添加到等待队列成功后,会进行调用 fullyRelease() 方法完全释放锁。代码如下:

// AbstractQueuedSynchronizer#ConditionObject
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;
    }
}

(3)isOnSyncQueue() 方法:检测此节点是否在同步队列中

如果不在,说明该线程还不具备竞争锁的资格,挂起线程继续等待,代码如下:

// AbstractQueuedSynchronizer#ConditionObject
public final void await() throws InterruptedException {
    //......
    
    //自旋检测此节点是否在同步队列中,如果不在,说明该线程还不具备竞争锁的资格,挂起线程继续等待
    while (!isOnSyncQueue(node)) {
        //挂起线程
        LockSupport.park(this);
        //线程被唤醒后,检查是否发生了中断
        //	被唤醒有两种原因:1是发生了中断,2是别的线程调用了signal
        if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
            break;  //如果发现线程已经中断了,则直接退出循环
    }
    
    //......
}

我们来看一下 isOnSyncQueue() 方法的实现:

// AbstractQueuedSynchronizer#ConditionObject
//检测此节点是否在同步队列
final boolean isOnSyncQueue(Node node) {
    //若是当前节点状态为CONDITION 或者说node前驱节点为null,说明节点不在同步队列里
    if (node.waitStatus == Node.CONDITION || node.prev == null)
        return false;
    //节点状态不为CONDITION 且node前驱节点存在,并且后继节点存在,则认为在同步队列里
    //这是为什么呢?因为调用该方法之前node节点插入的位置是等待队列的末尾,next域肯定为null,如果不为空,说明肯定不在等待队列中,即在CLH队列中
    if (node.next != null) // If has successor, it must be on queue
        return true;
    //从尾往头查找CLH队列,看是否存在node节点
    //后继节点不存在,可能是节点加入到同步队列尾部的时候,CAS修改tail指向不成功,因此此处再遍历同步队列直接比较节点是否相等
    return findNodeFromTail(node);
}

//判断CLH同步队列否存在node节点
private boolean findNodeFromTail(Node node) {
    //获取节点
    Node t = tail;
    //从后往前遍历查找是否存在node节点
    for (;;) {
        if (t == node)
            return true;
        if (t == null)
            return false;
        t = t.prev;
    }
}

(4)checkInterruptWhileWaiting() 方法:返回是否发生过中断以及中断的模式

// AbstractQueuedSynchronizer#ConditionObject
//返回是否发生过中断以及中断的模式: 如果在signal之前中断,则返回THROW_IE;如果在signal之后中断,则返回REINTERRUPT;如果没有中断,则为0。
private int checkInterruptWhileWaiting(Node node) {
    //如果发生线程被中断,执行方法transferAfterCancelledWait,否则返回0
    return Thread.interrupted() ?
        (transferAfterCancelledWait(node) ? THROW_IE : REINTERRUPT) :
    0;
}
//transferAfterCancelledWait(xx)返回true,表示需要抛出中断异常,返回false,表示只需要将中断标记位补上就好了
final boolean transferAfterCancelledWait(Node node) {
    //走到这,说明线程曾经被中断过,接下来就是要判断signal动作是否发生了
    //尝试修改节点状态
    if (compareAndSetWaitStatus(node, Node.CONDITION, 0)) {
        //成功,则加入到同步队列里
            //CAS 成功表明此时还没有发生signal()
        enq(node);
        return true;
    }
    //说明CAS失败,失败的原因是:signal()里已经将node状态改变了。
        //因此,此处只需要等待signal()将节点加入到同步队列即可。
    while (!isOnSyncQueue(node))
        Thread.yield();
    return false;
}

(5)unlinkCancelledWaiters() 方法:从等待队列里移除被取消的节点

虽然前面已经将节点加入到同步队列里,但是可能并没有将节点从等待队列里移除(没有调用signal的情况),因此这里需要检测一下。

// AbstractQueuedSynchronizer#ConditionObject
//清除条件队列中所有状态不为Condition的节点
private void unlinkCancelledWaiters() {
    //获取头结点
    Node t = firstWaiter;
    Node trail = null;
    //从头往尾清除掉等待状态不为CONDITION的节点
    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;
    }
}

(6)reportInterruptAfterWait() 方法:根据中断模式处理

中断模式 interruptMode == THROW_IE,表示需要抛出中断异常;interruptMode == REINTERRUPT,表示只需要将中断标记位补上。

// AbstractQueuedSynchronizer#ConditionObject
private void reportInterruptAfterWait(int interruptMode)
    throws InterruptedException {
    //直接抛出异常,发生了中断,但是还没有signal
    if (interruptMode == THROW_IE)
        throw new InterruptedException();
    else if (interruptMode == REINTERRUPT)
        //将中断标记位补上
        selfInterrupt();
}

3.1.3 ConditionObject#signal() 方法解析 

// AbstractQueuedSynchronizer#ConditionObject
public final void signal() {
     // 判断当前线程是否持有独占锁资源,如果未持有则直接抛出异常
    if (!isHeldExclusively())
          throw new IllegalMonitorStateException();
      Node first = firstWaiter;
      // 唤醒等待队列第一个节点的线程
      if (first != null)
          doSignal(first);
}

在这里,singal() 唤醒方法一共做了两件事:

  1. 判断当前线程是否持有独占锁资源,如果调用唤醒方法的线程未持有锁资源则直接抛出异常(共享模式下没有等待队列,所以无法使用Condition)。
  2. 唤醒等待队列中的第一个节点的线程,即调用doSignal(first)方法。

我们来看看 doSignal(first) 方法的实现:

// AbstractQueuedSynchronizer#ConditionObject
private void doSignal(Node first) {
    do {
        // 移除等待队列中的第一个节点,如果nextWaiter为空
        // 则代表着等待队列中不存在其他节点,那么将尾节点也置空
        if ( (firstWaiter = first.nextWaiter) == null)
            lastWaiter = null;
        first.nextWaiter = null;
    // 如果被通知上个唤醒的节点没有进入同步队列(可能出现被中断的情况),
    // 等待队列中还存在其他节点则继续循环唤醒后继节点的线程
    } while (!transferForSignal(first) &&
             (first = firstWaiter) != null);
}

// transferForSignal()方法
final boolean transferForSignal(Node node) {
    /*
     * 尝试修改被唤醒节点的waitStatus为0即初始化状态
     *      如果设置失败则代表着当前节点的状态不为CONDITION等待状态,
     *      而是结束状态了则返回false返回doSignal()继续唤醒后继节点
     *  为什么说设置失败则代表着节点不为CONDITION等待状态?
     *      因为可以执行到此处的线程必定是持有独占锁资源的,
     *      而此处使用的是cas机制修改waitStatus,失败的原因只有一种:
     *          预期值waitStatus不等于CONDITION
     */
    if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
        return false;
    // 快速追加到同步队列尾部,同时返回前驱节点p
    Node p = enq(node);
    // 判断前驱节点状态是否为结束状态或者在设置前驱节点状态为SIGNAL失败时,
    // 唤醒被通知节点内的线程
    int ws = p.waitStatus;
    if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
        // 唤醒node节点内的线程
        LockSupport.unpark(node.thread);
    return true;
}

在如上代码中,可以通过我的注释发现,doSignal() 也只做了三件事:

  1. 将被唤醒的第一个节点从等待队列中移除,然后再维护等待队列中 firstWaiter 和 lastWaiter 的指向节点引用。
  2. 将等待队列中移除的节点追加到同步队列尾部,如果同步队列追加失败或者等待队列中还存在其他节点的话,则继续循环唤醒其他节点的线程。
  3. 加入同步队列成功后,如果前驱节点状态已经为结束状态或者在设置前驱节点状态为 SIGNAL 失败时,直接通过 LockSupport.unpark() 唤醒节点内的线程。

至此,signal() 方法逻辑结束,不过需要注意的是:我们在理解 Condition 的等待/唤醒原理的时候需要将 await()/signal() 方法结合起来理解。在 signal() 逻辑完成后,被唤醒的线程则会从前面的 await() 方法的自旋中退出,因为当前线程所在的节点已经被移入同步队列,所以 while (!isOnSyncQueue(node)) 条件不成立,循环自然则终止,进而被唤醒的线程会调用 acquireQueued() 开始尝试获取锁资源。

3.2 Condition接口与Monitor对象等待/唤醒机制的区别

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值