引言
这个东西是我今天看多线程通信的时候无意中想到的,为什么像wait()、notify()、notifyAll()之类的线程间通信需要放在同步块种,换言之为什么要用synchronized。jie如果wait()方法不在同步块中,会怎么样嘞:
@Test
public void test() {
try {
new Object().wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
复制代码
结果是:
经过一番谷歌,参照了各路大神的博客,终于找到了答案。
Lost Wake-Up Problem
首先我们来举个例子,一个消费者线程、一个生产者线程。生产者的任务为count+1,然后唤醒消费者;消费者的任务为count-1,等到count为0时陷入沉睡。
生产者伪代码:
count++;
notify();
复制代码
消费者伪代码:
while(count <= 0){
wait();
count--;
}
复制代码
熟悉多线程的朋友应该一眼就看出来了问题,如果生产者和消费者的步骤混杂在一起会发生什么。
首先我们先假设count = 0,这个时候消费者检查count的值,发现count <= 0的条件成立;就在这个时候,发生了上下文切换,生产者进来了,噼噼啪啪一顿操作,把两个步骤都执行完了,也就是发出了通知,准备唤醒一个线程。这个时候消费者刚决定睡觉,还没睡呢,所以这个通知就会被丢掉。紧接着,消费者就睡过去了……
图片来自为什么wait()会这样
这就是所谓的Lost Wake-Up Problem
如何解决
现在我们应该能发现问题的根源在于,消费者在检查count到调用wait()之间,count就可能被改掉了。 那我们如何解决呢?
让消费者和生产者竞争一把锁,竞争到了的,才能够修改count的值。
于是乎生产者代码:
lock();
count++;
notify();
unlock();
复制代码
消费者代码:
lock();
while(count <= 0){
wait();
count--;
}
unlock();
复制代码
现在我们来看看,这样子真的解决了吗?
答案是毫无卵用,依旧会出现lost wake up问题,而且和无锁的表现是一样的。
因为wait()是释放锁然后等待获取锁,当然要先获得锁才行,但在这边连锁都莫得。
终极答案
所以,我们可以总结到,为了避免出现这种 lost wake up 问题,在这种模型之下,总应该将我们的代码放进去的同步块中。
Java强制我们的 wait()/notify() 调用必须要在一个同步块中,就是不想让我们在不经意间出现这种 lost wake up 问题。
不仅仅是这两个方法,包括 java.util.concurrent.locks.Condition 的 await()/signal() 也必须要在同步块中。
正解:
private Object obj = new Object();
private Object anotherObj = new Object();
@Test
public void produce() {
synchronized (obj) {
try {
//同步块要对当前线程负责,而不是anotherObj.notify();
obj.notify();
} catch (Exception e) {
e.printStackTrace();
}
}
}
复制代码
wait和notify的用法
wait()、notify()和notifyAll()
-
wait()、notify() 和 notifyAll()方法是本地方法,并且为 final 方法,无法被重写。
-
调用某个对象的 wait() 方法能让当前线程阻塞,并且当前线程必须拥有此对象的 monitor(即锁,或者叫管程)。
-
调用某个对象的 notify() 方法能够唤醒一个正在等待这个对象的 monitor 的线程,如果有多个线程都在等待这个对象的 monitor,则只能唤醒其中一个线程。
-
调用 notifyAll() 方法能够唤醒所有正在等待这个对象的monitor的线程。
具体应用
/**
* wait() && notify()方法
* 这两个方法是在Object中定义的,用于协调线程同步,比 join 更加灵活
*/
public class NotifyDemo {
public static void main(String[] args) {
//写两个线程 1.图片下载
Object obj=new Object();
Thread download=new Thread(){
public void run() {
System.out.println("开始下载图片");
for (int i = 0; i < 101; i+=10) {
System.out.println("down"+i+"%");
try {
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("图片下载成功");
synchronized (obj) {
obj.notify();//唤起
}
System.out.println("开始下载附件");
for (int i = 0; i < 101; i+=10) {
System.out.println("附件下载"+i+"%");
try {
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("附件下载成功");
}
};
//2.图片展示
Thread show=new Thread(){
public void run(){
synchronized (obj) {
try {
obj.wait();//阻塞当前
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("show:开始展示图片");
System.out.println("图片展示完毕");
}
}
};
download.start();
show.start();
}
}
复制代码