深入理解Condition的等待通知机制

1. Condition简介

任何一个java对象都天然继承于Object类,在线程间实现通信的往往会应用到Object的几个方法,比如wait(),wait(long timeout),wait(long timeout, int nanos)与notify(),notifyAll()几个方法实现等待/通知机制,同样的, 在java Lock体系下依然会有同样的方法实现等待/通知机制。从整体上来看Object的wait和notify/notify是与对象监视器配合完成线程间的等待/通知机制,而Condition与Lock配合完成等待通知机制,前者是java底层级别的,后者是语言级别的,具有更高的可控制性和扩展性。两者除了在使用方式上不同外,在功能特性上还是有很多的不同:

  1. Condition能够支持不响应中断,而通过使用Object方式不支持;
  2. Condition能够支持多个等待队列(new 多个Condition对象),而Object方式只能支持一个;
  3. Condition能够支持超时时间的设置,而Object不支持

参照Object的wait和notify/notifyAll方法,Condition也提供了同样的方法:

针对Object的wait方法

  1. void await() throws InterruptedException:当前线程进入等待状态,如果其他线程调用condition的signal或者signalAll方法并且当前线程获取Lock从await方法返回,如果在等待状态中被中断会抛出被中断异常;
  2. long awaitNanos(long nanosTimeout):当前线程进入等待状态直到被通知,中断或者超时
  3. boolean await(long time, TimeUnit unit)throws InterruptedException:同第二种,支持自定义时间单位
  4. boolean awaitUntil(Date deadline) throws InterruptedException:当前线程进入等待状态直到被通知,中断或者到了某个时间

针对Object的notify/notifyAll方法

  1. void signal():唤醒一个等待在condition上的线程,将该线程从等待队列中转移到同步队列中,如果在同步队列中能够竞争到Lock则可以从等待方法中返回。
  2. void signalAll():与1的区别在于能够唤醒所有等待在condition上的线程

2. 实现分析

ConditionObject是同步器AbstractQueuedSynchronizer的内部类,因为condition的操作需要获取相关的锁,所以作为同步器的内部类也是合理。每个condition 对象包含这一个队列(等待队列)。该队列作为Condition对象实现通知/等待功能的关键

2.1 等待队列

等待队列是一个FIFO的队列,在队列中每个节点都包含了一个线程引用,该线程就是在Condition对象上等待的线程。如果一个 线程调用了Condition.await() 方法,那么该线程将会释放锁、构造成节点加入等待队列并进入等待状态。

一个Condition包含一个队列,Condition拥有首节点(first waiter)和尾结点(last waiter)。当前线程调用Condition.await() 方法时,将会以当前线程构造节点并将节点从尾部加入等待队列。

/** First node of condition queue. */
private transient Node firstWaiter;
/** Last node of condition queue. */
private transient Node lastWaiter;
img

如上图所示,Condition拥有首尾节点的引用,新增节点只需要将原有尾结点nextwaiter指向它,并且更新尾结点即可。过程中没有使用CAS保证,原因在于调用await() 方法的线程必定是获取了锁的线程,该过程由锁来保证线程安全

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

img

如图所示,ConditionObject是AQS的内部类,因此每个ConditionObject能够访问到AQS提供的方法,相当于每个Condition都拥有所属同步器的引用。

2.2 等待---await()实现原理

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

如果从队列(同步队列和等待队列)的角度看await() 方法,当调用await() 方法时,相当于同步队列的首节点(当前锁的获取者)移动condition的等待队列中。await() 源码如下:

public final void await() throws InterruptedException {
    if (Thread.interrupted())
        throw new InterruptedException();
    // 1. 将当前线程包装成Node,尾插入到等待队列中
    Node node = addConditionWaiter();
    // 2. 释放当前线程所占用的lock,在释放的过程中会唤醒同步队列中的下一个节点
    int savedState = fullyRelease(node);
    int interruptMode = 0;
    // 3. 判断是否在同步队列
    while (!isOnSyncQueue(node)) {
        // 4. 当前线程进入到等待状态
        LockSupport.park(this);
        if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
            break;
    }
    // 5. 自旋等待获取到同步状态(即获取到lock)
    if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
        interruptMode = REINTERRUPT;
    if (node.nextWaiter != null) // clean up if cancelled
        unlinkCancelledWaiters();
    // 6. 处理被中断的情况
    if (interruptMode != 0)
        reportInterruptAfterWait(interruptMode);
}


//添加到等待队列
private Node addConditionWaiter() {
    Node t = lastWaiter;
    // If lastWaiter is cancelled, clean out.
    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
    lastWaiter = node;
    return node;
}

调用该方法的线程成功获取了锁。也就是同步队列的首节点。在第1步中将当前线程构造成节点加入等待队列,在第2步中会释放锁,唤醒同步队列的后继节点。然后进入 while 循环,然后通过第4步将该线程进入等待状态。从while退出有两种:1. 线程被中断 2. 线程进入同步队列

当等待队列中的节点被唤醒,唤醒的节点线程尝试获取同步状态,见第5步,若不是通过其他线程调用signal() 方法,而是对等待线程进行中断,则会抛出InterrupterException。

2.3 通知

调用condition的signal() 方法,将会唤醒在等待队列的首节点,在 唤醒节点之前,会将节点移到同步队列中

public final void signal() {
    //1. 先检测当前线程是否已经获取lock
    if (!isHeldExclusively())
        throw new IllegalMonitorStateException();
    //2. 获取等待队列中第一个节点,之后的操作都是针对这个节点
    Node first = firstWaiter;
    if (first != null)
        doSignal(first);
}

调用该方法的前置条件是当前线程必须获取了锁,即isHeldExclusively() 检查方法。当头结点不为null时执行doSignal()方法

private void doSignal(Node first) {
    do {
        if ( (firstWaiter = first.nextWaiter) == null)
            lastWaiter = null;
        //1. 将头结点从等待队列中移除
        first.nextWaiter = null;
        //2. while中transferForSignal方法对头结点做真正的处理
    } while (!transferForSignal(first) &&
             (first = firstWaiter) != null);
}

首先将头结点从队列中移除,再调用transferForSignal() 方法对头结点做真正的处理

final boolean transferForSignal(Node node) {
    /*
     * If cannot change waitStatus, the node has been cancelled.
     */
    //1. 更新状态为0
    if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
        return false;

    /*
     * Splice onto queue and try to set waitStatus of predecessor to
     * indicate that thread is (probably) waiting. If cancelled or
     * attempt to set waitStatus fails, wake up to resync (in which
     * case the waitStatus can be transiently and harmlessly wrong).
     */
    //2.将该节点移入到同步队列中去
    Node p = enq(node);
    int ws = p.waitStatus;
    if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
        LockSupport.unpark(node.thread);
    return true;
}

主要做了两件事:1.将头结点的状态更改为CONDITION;2.调用enq方法,将该节点尾插入到同步队列中

对于 ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL) 这个判断,基本上一直为false,那是不是意味着node的线程永远不会被唤醒?其实不是的,由于node已经加入到同步队列中,那这样在release方法中会对同步队列的线程进行唤醒

在等待队列中被唤醒的线程,将从await() 方法中的while循环中退出,进而调用同步器的accquireQueued() 方法加入到获取同步状态的竞争中。

signalAll() 方法

该方法相当于对等待队列的每个节点均指向一次signal() 方法,效果就是将等待队列中所有节点全部移到同步队列中

private void doSignalAll(Node first) {
    lastWaiter = firstWaiter = null;
    do {
        Node next = first.nextWaiter;
        first.nextWaiter = null;
        transferForSignal(first);
        first = next;
    } while (first != null);
}

你可能感兴趣的:(深入理解Condition的等待通知机制)