1. Condition的执行方式,是当在线程T1中调用await方法后,线程T1释放锁,并且将自己阻塞,等待唤醒,
  2.  
  3. 线程T2获取到锁后,开始做事,完毕后,调用Conditionsignal方法,唤醒线程T1,在t2执行完unlock后,线程T1恢复执行。

signalAll和signal很像,内部就是将Condition队列里所有的Node都加入到release队列中,仅此而已

代码如下:

  1. import org.joda.time.LocalDateTime;
  2.  
  3. import java.util.concurrent.TimeUnit;
  4. import java.util.concurrent.locks.Condition;
  5. import java.util.concurrent.locks.Lock;
  6. import java.util.concurrent.locks.ReentrantLock;
  7.  
  8. public class LockTest {
  9.  
  10. private static final String separator = " - ";
  11. private Lock lock = new ReentrantLock();
  12. private Condition condition = lock.newCondition();
  13.  
  14. private class T1 implements Runnable {
  15.  
  16. public void run() {
  17. try {
  18. lock.lock();
  19. System.out.println(LocalDateTime.now().toString("yyyy-MM-dd HH:mm:ss") + separator + Thread.currentThread() + " after lock");
  20. try {
  21. condition.await();//释放当前线程占用的锁,并阻塞当前线程,等待唤醒
  22. System.out.println(LocalDateTime.now().toString("yyyy-MM-dd HH:mm:ss") + separator + Thread.currentThread() + " after await");
  23. } catch (Exception ex) {
  24. ex.printStackTrace();
  25. }
  26. } finally {
  27. lock.unlock();
  28. System.out.println(LocalDateTime.now().toString("yyyy-MM-dd HH:mm:ss") + separator + Thread.currentThread() + " after unlock");
  29. }
  30.  
  31. }
  32.  
  33. }
  34.  
  35. private class T2 implements Runnable {
  36.  
  37. public void run() {
  38. try {
  39. lock.lock();
  40. System.out.println(LocalDateTime.now().toString("yyyy-MM-dd HH:mm:ss") + separator + Thread.currentThread() + " after lock");
  41. try {
  42. TimeUnit.SECONDS.sleep(1);
  43. condition.signal();
  44. System.out.println(LocalDateTime.now().toString("yyyy-MM-dd HH:mm:ss") + separator + Thread.currentThread() + " after signal");
  45. } catch (Exception ex) {
  46. ex.printStackTrace();
  47. }
  48. } finally {
  49. lock.unlock();
  50. System.out.println(LocalDateTime.now().toString("yyyy-MM-dd HH:mm:ss") + separator + Thread.currentThread() + " after unlock");
  51. }
  52. }
  53. }
  54.  
  55. private void run() throws InterruptedException {
  56. new Thread(new T1(), T1.class.getSimpleName()).start();
  57. TimeUnit.MILLISECONDS.sleep(50);
  58. new Thread(new T2(), T2.class.getSimpleName()).start();
  59. }
  60.  
  61. public static void main(String[] args) throws Exception {
  62. LockTest lt = new LockTest();
  63. lt.run();
  64. TimeUnit.MINUTES.sleep(1);
  65. }
  66.  
  67. }

输出:

  1. 2017-05-03 18:26:42 - Thread[T1,5,main] after lock
  2. 2017-05-03 18:26:42 - Thread[T2,5,main] after lock
  3. 2017-05-03 18:26:43 - Thread[T2,5,main] after signal
  4. 2017-05-03 18:26:43 - Thread[T2,5,main] after unlock
  5. 2017-05-03 18:26:43 - Thread[T1,5,main] after await
  6. 2017-05-03 18:26:43 - Thread[T1,5,main] after unlock

Condition的执行方式,是当在线程T1中调用await方法后,线程T1将释放锁,并且将自己沉睡,等待唤醒,

线程T2获取到锁后,开始做事,完毕后,调用Condition的signal方法,唤醒线程T1,线程T1恢复执行。

以上说明Condition是一个多线程间协调通信的工具类,使得某个,或者某些线程一起等待某个条件(Condition),只有当该条件具备( signal 或者 signalAll方法被带调用)时 ,这些等待线程才会被唤醒,从而重新争夺锁。

ReentrantLock(重入锁)是jdk的concurrent包提供的一种独占锁的实现。它继承自Dong Lea的 AbstractQueuedSynchronizer(同步器),确切的说是ReentrantLock的一个内部类继承了AbstractQueuedSynchronizer,ReentrantLock只不过是代理了该类的一些方法,可能有人会问为什么要使用内部类在包装一层? 我想是安全的关系,因为AbstractQueuedSynchronizer中有很多方法,还实现了共享锁,Condition(稍候再细说)等功能,如果直接使ReentrantLock继承它,则很容易出现AbstractQueuedSynchronizer中的API被无用的情况。

首先还是要明白,reentrantLock.newCondition() 返回的是Condition的一个实现,该类在AbstractQueuedSynchronizer中被实现,叫做newCondition()

  1. public Condition newCondition() {
  2. return sync.newCondition();
  3. }

它可以访问AbstractQueuedSynchronizer中的方法和其余内部类(AbstractQueuedSynchronizer是个抽象类,至于他怎么能访问,这里有个很奇妙的点,后面我专门用demo说明 )

现在,我们一起来看下Condition类的实现,还是从上面的demo入手,

为了方便书写,我将AbstractQueuedSynchronizer缩写为AQS

当await被调用时,代码如下:

  1. public final void await() throws InterruptedException {
  2. if (Thread.interrupted())
  3. throw new InterruptedException();
  4. Node node = addConditionWaiter(); // 将当前线程包装下后,
  5. // 添加到Condition自己维护的一个链表中。
  6. int savedState = fullyRelease(node);// 释放当前线程占有的锁,从demo中看到,
  7. // 调用await前,当前线程是占有锁的
  8.  
  9. int interruptMode = 0;
  10. while (!isOnSyncQueue(node)) {// 释放完毕后,遍历AQS的队列,看当前节点是否在队列中,
  11. // 不在 说明它还没有竞争锁的资格,所以将继续阻塞
  12. // 直到它被加入到队列中,聪明的你可能猜到了,
  13. // 没有错,在singal的时候加入不就可以了?
  14. LockSupport.park(this);
  15. if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
  16. break;
  17. }
  18. // 被唤醒后,重新开始正式竞争锁,同样,如果竞争不到还是会将自己沉睡,等待唤醒重新开始竞争。
  19. if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
  20. interruptMode = REINTERRUPT;
  21. if (node.nextWaiter != null)
  22. unlinkCancelledWaiters();
  23. if (interruptMode != 0)
  24. reportInterruptAfterWait(interruptMode);
  25. }

回到上面的demo,锁被释放后,线程1开始沉睡,这个时候线程因为线程1沉睡时,会唤醒AQS队列中的头结点,所所以线程2会开始竞争锁,并获取到,等待3秒后,线程2会调用signal方法,“发出”signal信号,signal方法如下:

  1. public final void signal() {
  2.     if (!isHeldExclusively())
  3.         throw new IllegalMonitorStateException();
  4.     Node first = firstWaiter; // firstWaiter为condition自己维护的一个链表的头结点,
  5.                               // 取出第一个节点后开始唤醒操作
  6.     if (first != null)
  7.         doSignal(first);
  8. }

说明下,其实Condition内部维护了等待队列的头结点和尾节点,该队列的作用是存放等待signal信号的线程,该线程被封装为Node节点后存放于此。

  1. public class ConditionObject implements Condition, java.io.Serializable {
  2. private static final long serialVersionUID = 1173984872572414699L;
  3. /** First node of condition queue. */
  4. private transient Node firstWaiter;
  5. /** Last node of condition queue. */
  6. private transient Node lastWaiter;

关键的就在于此,我们知道AQS自己维护的队列是当前等待资源的队列,AQS会在资源被释放后,依次唤醒队列中从前到后的所有节点,使他们对应的线程恢复执行。直到队列为空。

而Condition自己也维护了一个队列,该队列的作用是维护一个等待signal信号的队列,两个队列的作用是不同,事实上,每个线程也仅仅会同时存在以上两个队列中的一个,流程是这样的:

  1. 线程1调用reentrantLock.lock时,线程被加入到AQS的等待队列中。
  2. 线程1调用await方法被调用时,该线程从AQS中移除,对应操作是锁的释放。
  3. 接着马上被加入到Condition的等待队列中,以为着该线程需要signal信号。
  4. 线程2,因为线程1释放锁的关系,被唤醒,并判断可以获取锁,于是线程2获取锁,并被加入到AQS的等待队列中。
  5. 线程2调用signal方法,这个时候Condition的等待队列中只有线程1一个节点,于是它被取出来,并被加入到AQS的等待队列中。 注意,这个时候,线程1 并没有被唤醒。
  6. signal方法执行完毕,线程2调用reentrantLock.unLock()方法,释放锁。这个时候因为AQS中只有线程1,于是,AQS释放锁后按从头到尾的顺序唤醒线程时,线程1被唤醒,于是线程1回复执行。
  7. 直到释放所整个过程执行完毕。

可以看到,整个协作过程是靠结点在AQS的等待队列和Condition的等待队列中来回移动实现的,Condition作为一个条件类,很好的自己维护了一个等待信号的队列,并在适时的时候将结点加入到AQS的等待队列中来实现的唤醒操作。

看到这里,signal方法的代码应该不难理解了。

取出头结点,然后doSignal

  1. public final void signal() {
  2. if (!isHeldExclusively()) {
  3. throw new IllegalMonitorStateException();
  4. }
  5. Node first = firstWaiter;
  6. if (first != null) {
  7. doSignal(first);
  8. }
  9. }
  10.  
  11. private void doSignal(Node first) {
  12. do {
  13. if ((firstWaiter = first.nextWaiter) == null) // 修改头结点,完成旧头结点的移出工作
  14. lastWaiter = null;
  15. first.nextWaiter = null;
  16. } while (!transferForSignal(first) && // 将老的头结点,加入到AQS的等待队列中
  17. (first = firstWaiter) != null);
  18. }
  19.  
  20. final boolean transferForSignal(Node node) {
  21. /*
  22. * If cannot change waitStatus, the node has been cancelled.
  23. */
  24. if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
  25. return false;
  26.  
  27. /*
  28. * Splice onto queue and try to set waitStatus of predecessor to
  29. * indicate that thread is (probably) waiting. If cancelled or attempt
  30. * to set waitStatus fails, wake up to resync (in which case the
  31. * waitStatus can be transiently and harmlessly wrong).
  32. */
  33. Node p = enq(node);
  34. int ws = p.waitStatus;
  35. // 如果该结点的状态为cancel 或者修改waitStatus失败,则直接唤醒。
  36. if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
  37. LockSupport.unpark(node.thread);
  38. return true;
  39. }

可以看到,正常情况 ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL)这个判断是不会为true的,所以,不会在这个时候唤醒该线程。

只有到发送signal信号的线程调用reentrantLock.unlock()后因为它已经被加到AQS的等待队列中,所以才会被唤醒。

总结:

本文从代码的角度说明了Condition的实现方式,其中,涉及到了AQS的很多操作,比如AQS的等待队列实现独占锁功能,不过,这不是本文讨论的重点,等有机会再将AQS的实现单独分享出来。

http://www.importnew.com/9281.html

Condition的await-signal流程
Condition类图:

Condition接口包含了多种await方式和两个通知方法
ConditionObject实现了Condition接口,是AbstractQueuedSynchronizer的内部类
Reentrantlock的newCondition方法返回与某个lock实例相关的Condition对象

和release队列一样,Condition队列也是虚拟队列,每个Node通过nextWaiter进行关联。因为Condition Node要变为release Node才可以解除阻塞,所以不需要prevWaiter,这一点下面会有说明。

大概的整个过程是:

调用await的线程都会进入一个Condition队列。调用signal的线程每一次都会从firstWaiter开始找出未取消的Condition Node放到release队列里,然后调用signal的线程在await或者unlock的时候执行release方法才有机会将其解除阻塞。相对于lock-unlock,正常的流程要简单一些,但是对于中断处理会更为复杂。

先看看调用await()至阻塞的过程

如图所示,该过程可分为三个步骤:

新建Condition Node包装线程,加入Condition队列
释放当前线程占用的锁
阻塞当前线程
在阻塞当前线程之前,要判断Condition Node是否在release队列里。如果在的话则没必要阻塞,可直接参与锁竞争。关键代码如下:

signal方法更简单一些,就是从firstWaiter开始,找到一个没有取消的Node放入release队列。但是即使一开始找到的Node没被取消,但是入队列的时候也可能会被取消,因此代码对这个情况做了点特殊处理。我根据自己的理解将代码做了如下解释:
我们可以看到,signal方法只是将Node修改了状态,并没有唤醒线程。要将修改状态后的Node唤醒,一种是再次调用await(),一种是调用unlock()。这两个方法内部都会执行release方法对release队列里的Node解除阻塞,关于这点我在上一篇文章里已经说明了。

下面我把调用await()的线程被解除阻塞后的流程也画了一下:

以上就是await和signal的详细流程。signalAll和signal很像,内部就是将Condition队列里所有的Node都加入到release队列中,仅此而已。

http://www.cnblogs.com/jycboy/p/5623238.html

ReentrantLock Condition await signal 专题的更多相关文章

  1. 母鸡下蛋实例:多线程通信生产者和消费者wait/notify和condition/await/signal条件队列

    简介 多线程通信一直是高频面试考点,有些面试官可能要求现场手写生产者/消费者代码来考察多线程的功底,今天我们以实际生活中母鸡下蛋案例用代码剖析下实现过程.母鸡在鸡窝下蛋了,叫练从鸡窝里把鸡蛋拿出来这个 ...

  2. Java并发学习 & Executor学习 & 异常逃逸 & 同步互斥Best Practice & wait/notify, conditon#await/signal

    看了这篇文章:http://www.ciaoshen.com/2016/10/28/tij4-21/ 有一些Java并发的内容,另外查了一些资料. 朴素的Thread 首先,Java中关于线程Thre ...

  3. java并发编程——通过ReentrantLock,Condition实现银行存取款

         java.util.concurrent.locks包为锁和等待条件提供一个框架的接口和类,它不同于内置同步和监视器.该框架允许更灵活地使用锁和条件,但以更难用的语法为代价. Lock 接口 ...

  4. java 多线程 22 :生产者/消费者模式 进阶 利用await()/signal()实现

    java多线程15 :wait()和notify() 的生产者/消费者模式 在这一章已经实现了  wait/notify 生产消费模型 利用await()/signal()实现生产者和消费者模型 一样 ...

  5. java 多线程 Thread 锁ReentrantLock;Condition等待与通知;公平锁

    1,介绍: import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock;   在JA ...

  6. JUC在深入面试题——三种方式实现线程等待和唤醒(wait/notify,await/signal,LockSupport的park/unpark)

    一.前言 在多线程的场景下,我们会经常使用加锁,来保证线程安全.如果锁用的不好,就会陷入死锁,我们以前可以使用Object的wait/notify来解决死锁问题.也可以使用Condition的awai ...

  7. wait notify notifyAll await signal signalAll 的理解及示例

    从常见的一道面试题开始,题目的描述是这样子的: 有三个线程分别打印A.B.C,请用多线程编程实现,在屏幕上循环打印10次ABCABC- 网上大都教了你怎么去实现,其实我也写过一篇 https://bl ...

  8. Java并发控制:ReentrantLock Condition使用详解

    生产者-消费者(producer-consumer)问题,也称作有界缓冲区(bounded-buffer)问题,两个进程共享一个公共的固定大小的缓冲区.其中一个是生产者,用于将消息放入缓冲区:另外一个 ...

  9. ReentrantLock Condition 实现消费者生产者问题

    import java.util.LinkedList; import java.util.Queue; import java.util.concurrent.ExecutorService; im ...

随机推荐

  1. 深入理解Java内存模型--转载

    原文地址:http://www.infoq.com/cn/articles/java-memory-model-1 并发编程模型的分类 在并发编程中,我们需要处理两个关键问题:线程之间如何通信及线程之 ...

  2. gomail发送附件

    采用github.com/go-gomail/gomail/ 的邮件功能,可以发送附件 以及html文档,下面是其给出的demo,测试通过. package main //cmd: go get go ...

  3. webstorm快捷键(觉得有用,喜欢的话可以保存收藏哦)

    Ctrl+/ 或 Ctrl+Shift+/------------------------->>注释(// 或者/*…*/ ) Ctrl+X删除行 Ctrl+D复制行 Ctrl+G查找行 ...

  4. SFC梯形图编程

    SFC是居首的PLC编程语言 !: 不能为PLC所执行, 还需要其他的编程语言(梯形图) 转换成PLC可执行程序. 常用的SFC编程方法有三种 >  应用启保停电路进行 >  应用 置/复 ...

  5. Rotation--控件位置旋转

    今天想要完成一个按钮的动画,也就是随着手势在屏幕上的滑动,让按钮图片跟着旋转.刚开始的思路是,先把图片旋转以后,在把这个图片设置为imagebutton的背景.不过,会发现这个图片经过处理以后一直变形 ...

  6. Android代码模拟物理、屏幕点击事件

    一.应用中模拟物理和屏幕点击事件 例如,模拟对某个view的点击事件 private void simulateClick(View view, float x, float y) { long do ...

  7. golang recover panic 流程控制的可达与不可达

    --------------------------流程控制可达----------------------------- package main import "fmt" fu ...

  8. WEB前端--深入进去

    在网站开发这条道路上做专做精,一个专题一个专题的深入探索,一个盲区一个盲区的理解和记忆,终有大成的那一天的.

  9. 【Android开发经验】我们要友好的告诉用户,程序要崩溃了

        转载请注明出处:http://blog.csdn.net/zhaokaiqiang1992     尽管我们的程序在正式上线之前,都会经过严格的測试.从而保证程序的健壮性和良好的用户体验,可是 ...

  10. Dcloud开发webApp踩过的坑

    Dcloud开发webApp踩过的坑 一.总结 一句话总结:HTML5+扩展了JavaScript对象plus,使得js可以调用各种浏览器无法实现或实现不佳的系统能力,设备能力如摄像头.陀螺仪.文件系 ...