Condition接口

Posted 咸鱼

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Condition接口相关的知识,希望对你有一定的参考价值。

《Java并发编程艺术》读书笔记

Condition介绍

任意一个Java对象,都拥有一组监视器方法(定义在java.lang.Object中),主要包括wait()wait(long timeout)notify()以及notifyAll()方法,这些方法与synchronized同步关键字配合,可以实现等待/通知模式。Condition接口也提供了类似Object的监视器方法,与Lock配合可以实现等待/通知模式,但是这两者在使用方式以及功能特性上还是有差别的。Object上的监视器只有一个等待队列,但是Condition上可以有多个等待队列,每个Lock.newCondition都可以产生一个Condition对象,每个Condition对象关联到一个新的等待队列上。并且Condition支持响应中断,但是Object上的监视器不支持。

Condition接口示例

Condition定义了等待/通知两种类型的方法,当前线程调用这些方法时,需要提前获取到Condition对象关联的锁。Condition对象是由Lock对象(调用Lock对象的newCondition()方法)创建出来的,换句话说,Condition是依赖Lock对象的。

一般都会将Condition对象作为成员变量。当调用await()方法后,当前线程会释放锁并在此等待,而其他线程调用Condition对象的signal()方法,通知当前线程后,当前线程才从await()方法返回,并且在返回前已经获取了锁。

public interface Condition {

     // 当前线程进入等待状态直到被通知(signal)或被中断
    void await() throws InterruptedException;
    // 不响应中断等待,直到被通知(signal)
    void awaitUninterruptibly();
    // 等待指定时长直到被通知或中断或超时。
    long awaitNanos(long nanosTimeout) throws InterruptedException;
    // 等待指定时长直到被通知或中断或超时。
    boolean await(long time, TimeUnit unit) throws InterruptedException;
    // 当前线程进入等待状态直到被通知、中断或者到某个时间。如果没有到指定事件就被通知,方法返回true,否则false。 
    boolean awaitUntil(Date deadline) throws InterruptedException;
    // 唤醒一个等待在Condition上的线程,该线程从等待方法返回前必须获得与Condition相关联的锁  
    void signal();
    // 唤醒所有等待在Condition上的线程,该线程从等待方法返回前必须获得与Condition相关联的锁  
    void signalAll();
}

使用示例:

public static void main(String[] args) throws Exception{

    AtomicInteger count = new AtomicInteger(0) ;

    new Thread(()->{
        reentrantLock.lock();
        try {
            for(;count.intValue() < 10; count.incrementAndGet()) {
                Thread.sleep(1000L);
            }
            System.out.println(Thread.currentThread().getName() + "当前count=" +count.intValue() + "已达目标 准备发出信号量");
            condition.signal();
        }catch (Exception e) {
            e.printStackTrace();
        }finally {
            reentrantLock.unlock();
        }
    }).start();

    if(count.intValue() <= 10) {

        reentrantLock.lock();

        System.out.println(Thread.currentThread().getName() + "当前count=" + count.intValue() + "。准备await");
        condition.await();

        reentrantLock.unlock();
        System.out.println("当前count=" + count.intValue());
    }
}

Condition实现原理分析

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

下面将分析Condition的实现,主要包括:等待队列、等待和通知,下面提到的Condition如果不加说明均指的是ConditionObject。

等待队列

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

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

技术分享图片

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

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

技术分享图片

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

public final void await() throws InterruptedException {
    if (Thread.interrupted())
        throw new InterruptedException();
        
    // 将当前线程构造成一个node,加入到wait队列中。
    Node node = addConditionWaiter(); 
    //最终调用AQS的release释放同步锁
    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);
}

从上面的代码流程,我们知道主流程应该是:当一个线程调用await()后,首先将当前线程构造成一个node节点,加入到当前ConditionObject实例对象的同步队列尾部,释放当前线程占用的锁,然后阻塞等待另外线程unpark唤醒当前线程,当前线程唤醒后 尝试获取同步状态,获取成功返回,继续往下走。

通知

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

Condition的signal()方法具体实现如下:

public final void signal() {
    //判断当前线程是否是锁的拥有者
    if (!isHeldExclusively())
        throw new IllegalMonitorStateException();
    Node first = firstWaiter;
    if (first != null)
        //唤醒等待时间最长的节点(首节点)
        doSignal(first);
}

private void doSignal(Node first) {
    do {
        // 当前首节点出队列
        if ( (firstWaiter = first.nextWaiter) == null)
            lastWaiter = null;
        first.nextWaiter = null;
    } while (!transferForSignal(first) &&
             (first = firstWaiter) != null);
}

// 将condition的queue中节点移到同步队列中
// Transfers a node from a condition queue onto sync queue.
final boolean transferForSignal(Node node) {

    if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
        return false;
    // 将当前node加入到同步队列中,等待获取锁
    Node p = enq(node);
    int ws = p.waitStatus;
    //告诉前驱节点,在释放状态是通知自己。
    if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
        LockSupport.unpark(node.thread);
    return true;
}

调用该方法的前置条件是当前线程必须获取了锁,可以看到signal()方法进行了isHeldExclusively()检查,也就是当前线程必须是获取了锁的线程。接着获取等待队列的首节点,将其移动到同步队列并使用LockSupport唤醒节点中的线程。

当前线程加入等待队列的流程图:

技术分享图片

节点从等待队列移动到同步队列:

技术分享图片

通过调用同步器的enq(Node node)方法,等待队列中的头节点线程安全地移动到同步队列。当节点移动到同步队列后,当前线程再使用LockSupport唤醒该节点的线程。

被唤醒后的线程,将从await()方法中的while循环中退出(isOnSyncQueue(Node node)方法返回true,节点已经在同步队列中),进而调用同步器的acquireQueued()方法加入到获取同步状态的竞争中。

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

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

以上是关于Condition接口的主要内容,如果未能解决你的问题,请参考以下文章

Java多线程——Lock&Condition

Java并发多线程编程——Condition接口的理解及使用

并发编程系列之掌握Condition接口使用

并发编程系列之掌握Condition接口使用

Condition接口

接口 Condition