Java线程间通信之等待、通知

96
承香墨影
2017.03.09 23:56* 字数 2225

版权声明:

本账号发布文章均来自公众号,承香墨影(cxmyDev),版权归承香墨影所有。

允许有条件转载,转载请附带底部二维码。

一、前言

在程序中,线程存在的意义就是高效的完成某项任务,但是都是以一个独立的个体存在的,也就是说,如果不经过特殊处理,一个线程只能孤独的完成自己被既定的任务,然后完成任务之后,自我销毁。

如果能让多个线程之间进行通信协作,就可以跟高效的完成任务,这就是个人和团队的区别,团队如果配合得当,必定比个人的效率更高。那么,让多个线程之间通信,就是项基本的需求。

在Java中,线程间通信一般会使用等待(wait)、通知(noticy)的机制,接下来就围绕这个机制模型来进行讲解。

二、Java的等待/通知机制

1、比较low的方法

其实Java中是提供了线程间通信的机制,但是如果并不知道存在这样的机制,会怎么设计让多个线程间协作。

首先想到的应该是使用while(true) + sleep()的模型来做等待机制,在while循环里,判断某个共享变量是否满足条件,如果满足继续执行,如果不满足,使用sleep()等待,然后继续判断条件是否满足。

td-lowwait.png

虽然使用while()+sleep()的方式,可以在多线程之间实现了通信,但是有一个弊端,就是OneThread这个线程,会不停的使用轮询的方式判断条件是否满足,这样会非常的浪费CPU的资源。

而且轮询的间隔非常难把握,如果时间太短,在条件不满足之前,都是在空循环,而事时间间隔太长又没有办法及时的获取到状态的改变。

2、Java自己的等待、通知机制

既然使用while()+sleep()的方式非常的“不环保”,那么如何利用Java自己的等待、通知机制来完成上例子在中,线程等待的机制呢。

先简单说说,等待、通知机制,其实在生活中非常的常见,举个例子:

厨师和传菜员,厨师做完一道菜的时间是不确定的,那么如果有客人来吃饭,点了菜之后,厨师就开始做菜。在轮询的场景下,就是传菜员间隔五分钟过来后厨问一下:“菜做好了吗?”,没做好继续等五分钟再来问。而如果有更优雅的方式,厨师说,你也别五分钟来问一次了,怪累的,这样,菜做好了,我叫你。

这个优雅的方式,就是:好了,我叫你

在Java中,使用这种优雅的方式,需要借助两个方法:

  • wait():让当前线程,立即放弃锁,进入等待状态,直到其他获取锁的线程调用notify()方法再重新争抢锁。
  • notify():通知其他进入wait()的线程,可以继续运行了。

在使用的过程中,有几点需要注意:

  1. 这两个方法,都是Object类的方法,表示在当前的锁上进行等待和通知操作,也就是调用wait()notify()的对象,必须是加锁的对象。
  2. 这两个方法,都需要在已经获取到线程锁的情况下,在同步代码块内,才可以调用,否者会抛出IllegalMonitorStateException这个异常。
  3. wait()会立即放弃锁,进入等待状态。而notify()并不会立即放弃锁,而是会等同步代码块执行完成才放弃锁并通知其他线程。

接下来我们来改写上面的方法,使用wait等待,notify通知的方式。

hightWait.png

从上面的例子,验证了我上面总结的内容,notify()并不立即释放锁,而是会优先执行完当前的同步代码,再释放锁并通知出去。

3、wait/notify的缺陷

简单的wait/notify的机制是有缺陷的,wait必须等其他线程去notify才可以生效,而如果其他线程在此后并不来调用notify()的话,可能永远被等待下去,永远也得不到执行,然后就会发现线程“假死“了。

而在更多线程需要调用wait()或者notify()的时候,notify()只会随机的通知一个已经处于wait()状态的线程,去继续执行,而无法通知到所有处于等待状态的线程。

所以Java其实还提供了一些其他的api来解决这些问题:

  • wait(timeout):同样是等待,但是它设定了超时的机制,也就是说如果超过这个时间还没有被notify的话,会自动取消掉wait状态,继续去争抢锁得到执行权。
  • notifyAll():和名字一样,它会去通知所有处于wait状态的线程,可以开始执行了,但是如果有多个线程处于wait状态,也是需要争抢锁才能继续执行。

4、wait/notify机制的原理

从上面的讲解中也可以了解到,其实就是在不同的状态下相互切换。而每个锁对象,都会维护两个队列,一个是就绪队列,记录了将要获取锁的线程,处于就绪状态,获取到锁就可以立即执行,另外一个是阻塞队列,在阻塞队列中记录了等待被唤醒的线程,处于wait、sleep等状态的,当他们被唤醒之后,才会进入到就绪队列,等待分配锁资源后继续执行。

在有锁的情况下,大概运行的流程是这个样子的。

th-api.png

三、最后再举例子

在多线程的例子中,生产者、消费者真的是一个非常经典的例子。这里同样使用这个例子来说明问题。

生成者和消费者的例子,简单来说,就是一部分线程用于生成事件,而另外一部分线程用于消费事件。

1、单一生产者和消费者

没什么好说的,直接上例子。

proex1.png

上面的例子中,生产者负责生产一个字符串,然后消费者将其置空,同时都输出Log,输出结果如下。

p1_return.png

从Log中可以看到,生产者和消费者因为都是单一的,所以是生产一个就通知消费者消费一个。

2、多生产者、多消费者

上面在单一生产者和消费者的环境中,生产的速度和消费的速度是匹配的,这样的情况下,可以完美的一直运行下去。但是如果处于多生产者和消费者的情况下,会出现什么情况?

因为notify只会让一个wait的线程被现场调度器启动并执行,那么如果有多个生产者和消费者的话,可能一直是某一方单边被激活,那么就会多生产少消费或者少生产多消费,生产效率和消费效率不匹配的情况,其实是不利于效率的,总会有单边的线程被停滞。再极端一点的情况,每次都命中一边,这样的话,就没有生产者或者没有消费者了。

那么如何避免这样的问题?其实使用wait(timeout)或者notifyAll()都可以解决这样的问题,因为全部启动,大家每次都公平的争抢锁,基本上就不会存在只通知某一个而导致效率不匹配的问题,设置wait的超时时间,同样也可以保证所有的线程都有机会被重新进入就绪队列中。

这里就不再提供例子了,有兴趣的可以把上面的例子,启动多个生产者或者消费者看看效果。

公众号二维码.jpg
随笔
Web note ad 1