Java 并发编程 Condition

12 篇文章 0 订阅
12 篇文章 0 订阅

​填坑 Condition,配合 Lock 实现 线程间通信

2014拍摄于四川羌族藏族自治区郎木寺,前面的山就是甘肃了与四川只有一条两米宽的水流很大的小河相隔。

微信公众号

 

王皓的GitHub:https://github.com/TenaciousDWang

 

今天填以前挖的坑,说一下Condition,用于配合Lock实现线程间通信的同步辅助类,可替代以往我们使用 synchronized 关键字,配合 Object 的 wait()、notify() 系列方法可以实现等待/通知模式。

 

 

Condition是一个接口,JDK1.5之后加入,这种方式实现线程间协作更加安全和高效。因此通常来说比较推荐使用Condition,阻塞队列实际上是使用了Condition来模拟线程间协作。

 

为了便于理解,这里先简单用Condition中三个主要方法对应Object中使用的方法,先有一个大致的概念。

 

Conditon中的await()对应Object的wait()。

 

Condition中的signal()对应Object的notify()。

 

Condition中的signalAll()对应Object的notifyAll()。

 

Lock 接口中有个 newCondition() 方法。通过这个方法可以获得 Condition 对象,其实就是 ConditionObject。

 

 

先写一个简单的Demo来看一下Condition的使用,后面我们再分析一下Condition的实现。

 

 

先创建一个Lock对象,然后使用lock的newCondition获取Condition对象,接下来我们定义生产者与消费者,为了方便(懒)使用内部类直接写。

 

 

这里需要注意Condition的使用必须介于lock方法与unlock方法之间,消费者开始执行后,调用await方法进行阻塞,这时Consumer线程挂起,其实就是Consumer释放了锁,等待唤醒,被阻塞在Condition对象上的生产者线程此时获得了锁,生产者开始执行,执行结束后调用condition对象的signal方法用来唤醒被阻塞在condition对象的上的消费者线程,此时消费者线程才开始执行,打印消费者执行结束。我们来看一下执行后的打印结果。

 

 

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

 

Condition的实现类位于AQS类里,是AQS的一个内部类ConditionObject。

 

 

接下来我们来简单分析一下Condition的实现。

 

 

可以看到,等待队列和同步队列一样,使用的都是同步器 AQS 中的节点类 Node。同样拥有首节点firstWaiter和尾节点lastWaiter,每个 Condition 对象都包含着一个 FIFO 队列。

 

addConditionWaiter方法用于将当前线程包装成AQS里的Node类放入 FIFO 队列,其实就是一个链表。

 

接下来我们来看一下Consumer调用await方式时发生了什么。

 

 

首先使用addConditionWaiter方法将当前线程包装为Node类,然后调用fullyRelease(node)释放当前线程占有的锁。

 

释放锁完毕后,while (!isOnSyncQueue(node)) 循环判断当前Node是否存在于队列中。

 

 

如果不存在,说明还没有资格去竞争锁,所以继续沉睡等待加入队列后被唤醒。

 

LockSupport.park(this)为阻塞当前线程,如果if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)说明中断直接 break跳出。

 

如果存在则跳出当前循环,如何被放入队列,你可能已经知道了,我们后面来说,跳出循环后,重新开始正式竞争锁,同样,如果竞争不到还是会将自己沉睡,等待唤醒重新开始竞争。

 

接下来我们来看一下生产者执行完成后调用signal()来唤醒Consumer的过程。

 

 

可以唤醒等待队列的首节点(等待时间最长),唤醒之前会将该节点移动到同步队列中。firstWaiter为condition自己维护的一个链表的头结点,取出第一个节点后开始唤醒操作doSignal(first)。

 

 

调用 transferForSignal() 方法将节点移动到同步队列。

 

 

先改变Node状态为0,然后enq(node)加入同步队列。

 

 

最后LockSupport.unpark(node.thread);用来唤醒线程。

 

然后我们来看一下signalAll方法原理同signal差不多,只是将所有Node从Condition的FIFO队列中取出直到first为null为止,移动到同步队列中来,唤醒每个节点的线程来竞争锁。

 

 

关于"虚假唤醒",通常Condition与if条件判断语句结合使用,但是这里不建议使用if,例如:

 

if(!条件){

    condition.await(); //不满足条件,当前线程等待;

}

 

更推荐使用while来判断条件,例如:

 

while(!条件){

    condition.await(); //不满足条件,当前线程等待;

}

 

什么是虚假唤醒,我们来举一个例子,假设我们现在有一个生产者,两个消费者,一个队列。

 

1、1号消费者从队列中获取了一个元素,此时队列变为空。

 

2、2号消费者也想从队列中获取一个元素,但此时队列为空,2号消费者便只能进入阻塞condition.await(),等待队列非空。

 

3、这时,生产者将一个元素放入队列,并调用condition.signal()唤醒条件变量。

 

4、处于等待状态的2号消费者接收到生产者的唤醒信号,便准备解除阻塞状态去获取队列中的元素。

 

5、但是这时可能却出现以下情况:当2号消费者准备获得队列的锁,去获取队列中的元素时,此时1号消费者刚好执行完之前的元素操作,返回再去请求队列中的元素,1号消费者便获得队列的锁,检查到队列非空,就获取到了生产者刚刚放入队列的元素,然后释放队列锁。

 

6、等到2号消费者获得队列锁,判断发现队列仍为空,1号消费者已经拿走了这个元素,所以对于2号消费者而言,这次唤醒就是“虚假”的,它需要再次等待队列非空。

 

原因为多核处理器会唤醒阻塞在条件变量上的多个线程,如果用if判断,多个等待线程在满足if条件时都会被唤醒(虚假的),但实际上条件并不满足,生产者生产出来的消费品已经被第一个线程消费了。

 

以上虚假唤醒参考自:https://stackoverflow.com/questions/8594591/why-does-pthread-cond-wait-have-spurious-wakeups

 

最后来举几个使用Condition的经典例子,顺序打印ABC。

 

创建ABCLogic类,然后创建一个Lock,获取三个Condition,设置初始类型为A。

 

 

创建打印A,B,C的方法。

 

 

最后使用main方法将A,B,C各打印五次。

 

 

执行结果为:

 

 

最后我们再使用经典生产者与消费者模型来写一下实例。

 

首先用传统synchronized与wait和notify来实现。

 

 

接下来来我们使用Condition来实现。

 

 

 

最后再复习一下使用阻塞队列来实现生产者与消费者模型。

 

 

使用阻塞式队列就可以不用像非阻塞式队列需要手动挂起与唤醒,其内部实现已经帮我们全部实现了。

 

以上就是Java并发编程Condition的相关知识点。

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值