Java并发编程之Condition详解

简介

在Java程序中,任意一个Java对象,都拥有一组监视器方法(定义在java.lang.Object类上),主要包括wait()、wait(long)、notify()、notifyAll()方法,这些方法与synchronized关键字配合,可以实现等待/通知模式。Condition接口也提供了类似Object的监视器方法,与Lock配合可以实现等待/通知模式,但是这两者在使用方式以及功能特性上还是有区别的。Object的监视器方法与Condition接口对比如下:

对比项

Object监视器方法

Condition

前置条件

获取对象的监视器锁

调用Lock.lock()获取锁

调用Lock.newCondition()获取Condition对象

调用方法

直接调用

如:object.wait()

直接调用

如:condition.await()

等待队列个数

一个

多个

当前线程释放锁并进入等待队列

支持

支持

当前线程释放锁并进入等待队列,在等待状态中不响应中断

不支持

支持

当前线程释放锁并进入超时等待状态

支持

支持

当前线程释放锁并进入等待状态到将来的某个时间

不支持

支持

唤醒等待队列中的一个线程

支持

支持

唤醒等待队列中的全部线程

支持

支持

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的方法以及描述如下:

方法名称

描  述

void await() throws InterruptedException

当前线程进入等待状态直到被通知(signal)或中断。

void awaitUninterruptibly()

当前线程进入等待状态直到被通知,该方法不响应中断。

long awaitNanos(long nanosTimeout) throws InterruptedException

当前线程进入等待状态直到被通知、中断或者超时,返回值表示剩余超时时间。

boolean awaitUntil(Date deadline) throws InterruptedException

当前线程进入等待状态直到被通知、中断或者到某个时间。如果没有到指定时间就被通知,方法返回true,否则,表示到了指定时间,返回false。

void signal()

唤醒一个等待在Condition上的线程,该线程从等待方法返回前必须获得与Condition相关联的锁。

void signalAll()

唤醒所有等待在Condition上的线程,能够从等待方法返回的线程必须获得与Condition相关联的锁。

Condition的实现分析

可以通过Lock.newCondition()方法获取Condition对象,而我们知道Lock对于同步状态的实现都是通过内部的自定义同步器实现的,newCondition()方法也不例外,所以,Condition接口的唯一实现类是同步器AQS的内部类ConditionObject,因为Condition的操作需要获取相关的锁,所以作为同步器的内部类也比较合理,该类定义如下:

public class ConditionObject implements Condition, java.io.Serializable

每个Condition对象都包含着一个队列(以下称为等待队列),该队列是Condition对象实现等待/通知功能的关键。

等待队列

等待队列是一个FIFO队列,在队列中的每个节点都包含了一个线程引用,该线程就是在Condition对象上等待的线程,如果一个线程调用了Condition.await()方法,那么该线程将会释放锁、构造成节点加入等待队列并进入等待状态。事实上,节点的定义复用了AQS中Node节点的定义,也就是说,同步队列和等待队列中节点类型都是AQS的静态内部类AbstractQueuedSynchronized.Node。

一个Condition包含一个等待队列,Condition拥有首节点(firstWaiter)和尾节点(lastWaiter)。当前线程调用Condition.await()方法之后,将会以当前线程构造节点,并将节点从尾部加入等待队列,等待队列的基本结构如下图所示:


Condition拥有首尾节点的引用,而新增节点只需要将原来的尾节点nextWaiter指向它,并且更新尾节点即可。上述更新过程不需要CAS保证,原因在于调用await()方法的线程必定是获取了锁的线程,也就是说该过程是由锁来保证线程安全的。

在Object的监视器模型上,一个对象拥有一个同步队列和等待队列,而并发包中的Lock(更确切地说是同步器)拥有一个同步队列和多个等待队列,其对应关系如下图所示:


Condition的实现是同步器的内部类,因此每个Condition实例都能够访问同步器提供的方法,相当于每个Condition都拥有所属同步器的引用。

等待

调用Condition的await()方法会使当前线程进入等待状态,同时线程状态变为等待状态,当从await()方法返回时,当前线程一定获取了Condition相关联的锁。

如果从队列(同步队列和等待队列)的角度看await()方法,当调用await()方法时,相当于同步队列的首节点(获取了锁的节点)移动到Condition的等待队列中。

Condition的await()方法如下:

public final void await() throws InterruptedException {
    // 检测线程中断状态
    if (Thread.interrupted())
        throw new InterruptedException();
    // 将当前线程包装为Node节点加入等待队列
    Node node = addConditionWaiter();
    // 释放同步状态,也就是释放锁
    int savedState = fullyRelease(node);
    int interruptMode = 0;
    // 检测该节点是否在同步队中,如果不在,则说明该线程还不具备竞争锁的资格,则继续等待
    while (!isOnSyncQueue(node)) {
        // 挂起线程
        LockSupport.park(this);
        if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
            break;
    }
    // 竞争同步状态
    if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
        interruptMode = REINTERRUPT;
    // 清理条件队列中的不是在等待条件的节点
    if (node.nextWaiter != null) // clean up if cancelled
        unlinkCancelledWaiters();
    if (interruptMode != 0)
        reportInterruptAfterWait(interruptMode);
}

调用该方法的线程是成功获取了锁的线程,也就是同步队列中的首节点,该方法会将当前线程构造节点并加入等待队列中,然后释放同步状态,唤醒同步队列中的后继节点,然后当前线程会进入等待状态。

加入等待队列是通过addConditionWaiter()方法来完成的:

private Node addConditionWaiter() {
    // 尾节点
    Node t = lastWaiter;
    // 尾节点如果不是CONDITION状态,则表示该节点不处于等待状态,需要清理节点
    if (t != null && t.waitStatus != Node.CONDITION) {
        unlinkCancelledWaiters();
        t = lastWaiter;
    }
    // 根据当前线程创建Node节点
    Node node = new Node(Thread.currentThread(), Node.CONDITION);
    // 将该节点加入等待队列的末尾
    if (t == null)
        firstWaiter = node;
    else
        t.nextWaiter = node;
    lastWaiter = node;
    return node;
}

如果从队列的角度看,当前线程加入到Condition的等待队列,如下图所示:


将当前线程加入到等待队列之后,需要释放同步状态,该操作通过fullyRelease(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;
    }
}

线程释放锁之后,我们需要通过isOnSyncQueue(Node)方法不断自省地检查其对应节点是否在同步队列中:

final boolean isOnSyncQueue(Node node) {
    // 节点状态为CONDITION,或者前驱节点为null,返回false
    if (node.waitStatus == Node.CONDITION || node.prev == null)
        return false;
    // 后继节点不为null,那么肯定在同步队列中
    if (node.next != null) // If has successor, it must be on queue
        return true;
    
    return findNodeFromTail(node);
}

若节点不在同步队列中,则挂起当前线程,若线程在同步队列中,且获取了同步状态,可能会调用unlinkCancelledWaiters()方法来清理等待队列中不为CONDITION 状态的节点:

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的signal()方法,将会唤醒在等待队列中等待时间最长的节点(首节点),在唤醒节点之前,会将节点移到同步队列中。Condition的signal()方法如下所示:

public final void signal() {
    // 判断是否是当前线程获取了锁
    if (!isHeldExclusively())
        throw new IllegalMonitorStateException();
    // 唤醒等待队列的首节点
    Node first = firstWaiter;
    if (first != null)
        doSignal(first);
}

该方法最终调用doSignal(Node)方法来唤醒节点:

private void doSignal(Node first) {
    do {
        // 把等待队列的首节点移除之后,要修改首结点
        if ( (firstWaiter = first.nextWaiter) == null)
            lastWaiter = null;
        first.nextWaiter = null;
    } while (!transferForSignal(first) &&
                (first = firstWaiter) != null);
}

将节点移动到同步队列是通过transferForSignal(Node)方法完成的:

final boolean transferForSignal(Node node) {
    // 尝试将该节点的状态从CONDITION修改为0
    if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
        return false;
    
    // 将节点加入到同步队列尾部,返回该节点的前驱节点
    Node p = enq(node);
    int ws = p.waitStatus;
    // 如果前驱节点的状态为CANCEL或者修改waitStatus失败,则直接唤醒当前线程
    if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
        LockSupport.unpark(node.thread);
    return true;
}

节点从等待队列移动到同步队列的过程如下图所示:


被唤醒后的线程,将从await()方法中的while循环中退出(因为此时isOnSyncQueue(Node)方法返回true),进而调用acquireQueued()方法加入到获取同步状态的竞争中。

成功获取了锁之后,被唤醒的线程将从先前调用的await()方法返回,此时,该线程已经成功获取了锁。

Condition的signalAll()方法,相当于对等待队列的每个节点均执行一次signal()方法,效果就是将等待队列中的所有节点移动到同步队列中。

相关博客

Java并发编程之synchronized详解

AbstractQueuedSynchronizer简介

AbstractQueuedSynchronizer同步队列详解

AbstractQueuedSynchronizer独占式同步状态获取与释放

AbstractQueuedSynchronizer共享式同步状态获取与释放

参考资料

方腾飞:《Java并发编程的艺术》

评论 4 您还未登录,请先 登录 后发表或查看评论
相关推荐

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
©️2022 CSDN 皮肤主题:编程工作室 设计师:CSDN官方博客 返回首页

打赏作者

DivineH

你的鼓励将是我创作的最大动力

¥2 ¥4 ¥6 ¥10 ¥20
输入1-500的整数
余额支付 (余额:-- )
扫码支付
扫码支付:¥2
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值