java的wait/notify的通知机制可以用来实现线程间通信。wait表示线程的等待,调用该方法会导致线程阻塞,直至另一线程调用notify或notifyAll方法才可另其继续执行。经典的生产者、消费者模式即是使用wait/notify机制得以完成。
一、举个例子
Java线程进行通信,我们这里想到的是用wait和notify的模式,那么这里先举个例子,流程如下:
1、客人点菜,点完菜就等待
2、老板收到菜名,做菜,做好菜后通知客人吃
3、客人接收到通知,开始吃菜。3、客人接收到通知,开始吃菜。
我这里用一个很简单的例子来处理上面的逻辑,如下;
/**
* 线程之间的消息通知
* @author Java全栈suibibk.com
*
*/
public class WaitAndNotify {
private Object notify = new Object();
public static void main(String[] args) {
WaitAndNotify waitAndNotify = new WaitAndNotify();
//客人开始点菜
new Thread(waitAndNotify.new Guest()).start();
new Thread(waitAndNotify.new Boss()).start();
}
//客人必须等老板做好饭后才能吃饭
class Guest implements Runnable{
@Override
public void run() {
synchronized (notify) {
//开始点菜
try {
System.out.println("客人:老板,我要吃酸菜鱼。。。。");
//等待老板做,这里是无限期等待,老板没做好,我就不走
notify.wait();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
}
//老板必须等客人点餐后才能做饭
class Boss implements Runnable{
@Override
public void run(){
synchronized (notify) {
System.out.println("老板:好勒!");
System.out.println("老板:杀鱼、热油、加入配菜、出锅...");
try {
//休息十秒钟
Thread.sleep(10000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println("。。。。。。。。过了十分钟!");
System.out.println("老板:客人,您的酸菜鱼做好了,请慢用!!");
//通知客人已经做好了
notify.notify();
}
}
}
}
运行结果如下
老板:好勒!
老板:杀鱼、热油、加入配菜、出锅...
。。。。。。。。过了十分钟!
老板:客人,您的酸菜鱼做好了,请慢用!!
二、改为超时模式
只需要把wait调用改为如下即可
public void run() {
synchronized (notify) {
//开始点菜
try {
System.out.println("客人:老板,我要吃酸菜鱼。。。。");
//改为一秒超时
notify.wait(1000);
System.out.println("这么久不上菜,劳资不吃了");
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
再次运行,却发现,明明过了一秒钟,为啥后面一句迟迟不打印?
三、原因分析
其实我们知道,在多线程通信中有两个队列,一个是同步队列,一个是等待队列。我们来大概分析一下上面的流程:
- 1、客人线程获取notify锁,此时老板线程进入同步队列
- 2、客人线程执行wait方法,此时将锁交出,自己进入等待对列
- 3、老板线程获得notify锁,开始做酸菜鱼,做了十秒钟
- 4、1秒钟后,客人线程的wait方法超时,但是没有办法,超时只是从等待对列出来,但是最多进入同步队列,必须等劳保线程执行完把notify对象的锁释放,客人线程才能够执行。
所以,就算超时了,是否能够获取到锁,还是得去竞争。也就是notify和notifyAll都不释放锁,只是唤醒正在等待这个对象的monitor的线程,但其是否能得到monitor取决于cpu调度。
一个线程被唤醒可能有一下四种情况
- 其它的线程调用 obj.notify(),且当前线程 T,正好是被选中唤醒的。
- 其它的线程调用 obj.notifyAll()。
- 其它线程中断 T。
- 指定的等待时间(timeout)超时,(时间精度会有些误差)。
但是上面分析,唤醒是唤醒,能否获得到锁,还是得竞争。
四、有人就会想,那我老板线程不用notify锁不就行了
试一下,会发现报如下错误:
java.lang.IllegalMonitorStateException
为什么呢,首先我们了解下wait和notify为什么要放在synchronized里面?
wait方法的语义有两个,
释放当前的对象锁、
使得当前线程进入阻塞队列,使得当前线程进入阻塞队列,
而这些操作都和监视器是相关的,所以wait必须要获得一个监视器锁。notify也一样,它是唤醒一个线程,所以需要知道待唤醒的线程在哪里,就必须找到这个对象获取这个对象的锁然后去到这个对象的等待队列去唤醒一个线程。
所以,必须用同一个锁啦,不然唤醒不了,根本找不到要唤醒的线程。
五、等待/通知的经典范式
上面的例子没用使用while循环把wait包裹起来在多线程的情况下是有问题的,因为唤醒线程不一定获得到锁,并且就算获取的到锁也不一定能够满足条件,如果不满足条件还是需要交出去,等待通知的经典范式应该如下所示:
等待方遵循如下原则
1、获取对象的锁
2、如果条件不满足,那么调用对象的wait()方法,被通知后仍要检查条件
3、条件满足则执行对应的逻辑
伪代码如下
synchronized(对象){
while(条件不满足){
对象.wait()
}
对应的处理逻辑
}
通知方遵循如下原则
1、获取对象的锁
2、改变条件
3、通知所有等待在对象上的线程。
伪代码如下
synchronized(对象){
改变条件
对象.notifyAll();
}
六、总结
由上分析,我们如果用了超时等待,就不要想着唤醒自己的线程一把自己唤醒就能够得到锁,也不要想着等待超时就能马上执行,都已经把锁交出去了,当然要重新竞争啦。一般都是用notifyAll和while循环来处理。