十三. 等待通知机制之Condition接口

布满荆棘的人生 2024-03-29 15:08 180阅读 0赞

前言

Condition接口定义了一组方法用于配合Lock实现等待/通知模式,与之作为对比的是,用于配合synchronized关键字实现等待/通知模式的定义在java.lang.Object上的监视器方法wait()notify()等。

正文

通常基于LocknewCondition()方法创建Condition对象并作为对象成员变量来使用,如下所示。

  1. public class MyCondition {
  2. private Lock lock = new ReentrantLock();
  3. private Condition condition = lock.newCondition();
  4. ......
  5. }

队列同步器AbstractQueuedSynchronizer的内部类ConditionObject实现了Condition接口,后续将基于ConditionObject的实现进行讨论。首先给出Condition接口定义的方法。

  1. public interface Condition {
  2. void await() throws InterruptedException;
  3. void awaitUninterruptibly();
  4. long awaitNanos(long nanosTimeout) throws InterruptedException;
  5. boolean await(long time, TimeUnit unit) throws InterruptedException;
  6. boolean awaitUntil(Date deadline) throws InterruptedException;
  7. void signal();
  8. void signalAll();
  9. }

上述方法的说明如下表所示。


































方法 说明
await() 调用此方法的线程进入等待状态,响应中断,也可以被signal()signalAll()方法唤醒并返回,唤醒并返回前需要获取到锁资源。
awaitUninterruptibly() await(),但不响应中断。
awaitNanos() await(),并可指定等待时间,响应中断。该方法有返回值,表示剩余等待时间。
awaitUntil() await(),并可指定等待截止时间点,响应中断。该方法有返回值,true表示没有到截止时间点就被唤醒并返回。
signal() 唤醒等待队列中的第一个节点。
signalAll() 唤醒等待队列中的所有节点。

针对上面的方法再做两点补充说明:

  • 等待队列是Condition对象内部维护的一个FIFO队列,当有线程进入等待状态后会被封装成等待队列的一个节点并添加到队列尾;
  • 从等待队列唤醒并返回的线程一定已经获取到了与Condition对象关联的锁资源,Condition对象与创建Condition对象的锁关联。

下面将结合ConditionObject类的源码来对等待/通知模式的实现进行说明。await()方法的实现如下所示。

  1. public final void await() throws InterruptedException {
  2. if (Thread.interrupted())
  3. throw new InterruptedException();
  4. // 基于当前线程创建Node并添加到等待队列尾
  5. // 这里创建的Node的等待状态为CONDITION,表示等待在等待队列中
  6. Node node = addConditionWaiter();
  7. // 释放锁资源
  8. int savedState = fullyRelease(node);
  9. int interruptMode = 0;
  10. // Node从等待返回后会被添加到同步队列中
  11. // Node成功被添加到同步队列中则退出while循环
  12. while (!isOnSyncQueue(node)) {
  13. LockSupport.park(this);
  14. if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
  15. break;
  16. }
  17. // 让Node进入自旋状态,竞争锁资源
  18. if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
  19. interruptMode = REINTERRUPT;
  20. // 遍历等待队列,将已经取消等待的节点从等待队列中去除链接
  21. if (node.nextWaiter != null)
  22. unlinkCancelledWaiters();
  23. // Node如果是被中断而从等待返回,则抛出中断异常
  24. if (interruptMode != 0)
  25. reportInterruptAfterWait(interruptMode);
  26. }

理解await()方法的整个执行流程前,先看一下等待队列的一个示意图,如下所示。

在这里插入图片描述

Condition对象分别持有等待队列头节点和尾节点的引用,新添加的节点会添加到等待队列尾,同时lastWaiter会指向新的尾节点。

现在回到await()方法,在await()方法中,会做如下事情。

  • 首先,会基于当前线程创建Node并添加到等待队列尾,创建Node有两个注意点:1. 这里创建的Node复用了同步队列中的Node定义;2. 在创建Node前会判断等待队列的尾节点是否已经结束等待(即等待状态不为Condition),如果是则会遍历等待队列并将所有已经取消等待的节点从等待队列中去除链接;
  • 然后,当前线程会释放锁资源,并基于LockSupport.park()进入等待状态;
  • 再然后,当前线程被其它线程唤醒,或者当前线程被中断,无论哪种方式,当前线程对应的Node都会被添加到同步队列尾并进入自旋状态竞争锁资源,注意,此时当前线程对应的Node还存在于等待队列中;
  • 再然后,判断当前线程对应的Node是否是等待队列尾节点,如果不是则触发一次清除逻辑,即遍历等待队列,将已经取消等待的节点从等待队列中去除链接,如果是等待队列尾节点,那么当前线程对应的Node会在下一次创建Node时从等待队列中被清除链接;
  • 最后,判断当前线程从等待返回的原因是否是因为被中断,如果是,则抛出中断异常。

上面讨论了等待的实现,下面再结合源码看一下通知的实现。首先是signal()方法,如下所示。

  1. public final void signal() {
  2. if (!isHeldExclusively())
  3. throw new IllegalMonitorStateException();
  4. Node first = firstWaiter;
  5. if (first != null)
  6. doSignal(first);
  7. }

signal()方法可知,调用signal()方法的线程需要持有锁,其次signal()方法会唤醒等待队列的头节点,即可以理解为唤醒等待时间最久的节点。下面再看一下signalAll()方法,如下所示。

  1. public final void signalAll() {
  2. if (!isHeldExclusively())
  3. throw new IllegalMonitorStateException();
  4. Node first = firstWaiter;
  5. if (first != null)
  6. doSignalAll(first);
  7. }

可以发现,signalAll()signal()方法大体相同,只不过前者最后会调用doSignalAll()方法来唤醒所有等待节点,后者会调用doSignal()方法来唤醒头节点,下面以doSignal()方法进行说明。

  1. private void doSignal(Node first) {
  2. do {
  3. if ( (firstWaiter = first.nextWaiter) == null)
  4. lastWaiter = null;
  5. first.nextWaiter = null;
  6. } while (!transferForSignal(first) &&
  7. (first = firstWaiter) != null);
  8. }

实际就是在transferForSignal()方法中将头节点添加到同步队列尾,然后再调用LockSupport.unpark()进行唤醒。

总结

Java并发编程的艺术》5.6小节对两者的差异进行了对比和总结,这里直接贴过来作参考。









































对比项 Object Monitor Methods Condition
当前线程释放锁并进入等待状态 支持 支持
当前线程释放锁并进入等待状态,等待过程中不响应中断 不支持 支持
当前线程释放锁并进入超时等待状态 支持 支持
当前线程释放锁并等待至将来某个时间点 不支持 支持
唤醒队列中的一个线程 支持 支持
唤醒队列中的多个线程 支持 支持

发表评论

表情:
评论列表 (有 0 条评论,180人围观)

还没有评论,来说两句吧...

相关阅读

    相关 java线程等待/通知机制

    等待通知机制 一个线程修改了对象的值,而另一个线程感知了变化并进行相应操作,整个过程开始于一个线程而最终执行又是另一个线程。前者是生产者,后者是消费者,这种模式隔离了“做