为什么wait和notify要在同步块内执行?

概述

​ 今天看到一个很值得深思的问题”为什么wait(),notify(),notifyAll()方法需要在同步块中执行?“,我的第一反应是wait()方法功能就是释放对象锁进行等待队列,如果没有持有锁,那如何释放呢?很有道理的样子,但其实并没有回答jdk为什么要这么做,今天我们就探讨下具体原因。

wait和notify的使用

​ 我们先对wait和notify进行介绍,wait()和notify()方法是Object类的两个native方法,是java线程间通信的方式,在一个线程中调用对象的wait()方法,会使得线程阻塞在该对象锁上;调用notify()方法会唤醒在这个对象上阻塞的方法(具体的,该方法会随机唤醒一个阻塞在对象上的线程)。这两个方法使用时总是成对出现,并且必须在同步块中执行。下面看下源码中的相关定义:

1
2
3
4
5
6
7
8
//将线程在挂起,进入阻塞状态,timeout 毫秒时间内没有唤醒会自动被唤醒
public final native void wait(long timeout) throws InterruptedException;
//将线程挂起,进入阻塞状态
public final void wait() throws InterruptedException;
//随机唤醒一个阻塞在该对象上的线程
public final native void notify();
//唤醒所有阻塞在对象上的线程
public final native void notifyAll();

​ 各个方法如何使用都已经注释在代码中,下面我们看一个实际使用的简单示例,在线程thread中挂起线程,然后在主线程中调用notify唤醒线程。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
/**
* Created by yuanqiongqiong on 2019/4/12.
*/
public class WaitTest {
private static Logger logger = LoggerFactory.getLogger(WaitTest.class);

private static integer object = new Object();
private static boolean flag = true;//避免出现因为调度问题先执行主线程情况

public static void main(String [] args) {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
if (flag) {
synchronized (object) {
flag = false;
try {
logger.info("挂起线程");
object.wait();
logger.info("线程被唤醒");
} catch (InterruptedException e) {
logger.error("exception = {}", e);
}
}
}
}
});
thread.start();
logger.info("在主线程中");
while (flag) {}//忙等
synchronized (object) {
logger.info("调用notify");
object.notify();
}
}
}

​ 执行结果为:

1
2
3
4
22:27:40.842 [Thread-0] INFO com.qiongqiong.WaitTest - 挂起线程
22:27:40.842 [main] INFO com.qiongqiong.WaitTest - 在主线程中
22:27:40.845 [main] INFO com.qiongqiong.WaitTest - 调用notify
22:27:40.845 [Thread-0] INFO com.qiongqiong.WaitTest - 线程被唤醒

​ 如前所述,wait和notify必须在同步块里执行,并且这个synchronized锁定的对象一定是调用wait和notify对象。否则,会抛出java.lang.IllegalMonitorStateExceptiony异常,这是jdk做了一层强制校验。

问题分析

​ 我们首先从JDK实现的角度分析,我们知道synchronized底层使用monitorenter和monitorexit指令(查看class文件也可以看到),这块会获取对象锁(monitor)。而wait和notify相关的native方法要求调用时必须持有对象锁。

​ 其次,我们假设如果wait()和notify()不在同步块中并且可以执行上述示例可能出现的结果,thread线程和main线程按照下面顺序执行。

thread线程 main线程
logger.info(“挂起线程”);
logger.info(“调用notify”);
object.notify();
object.wait();

​ 当main线程执行notify()时,thread线程还没有调用wait,即还处于运行状态,这是这条语句调用没有任何作用。但是,wait()方法执行后,thread线程将永远处于阻塞状态,这个问题就是多线程中的”lost wake up”问题,而jdk为了避免该问题的出现,强制要求我们使用wait和notify时必须持有该对象的锁。

​ 以上就是wait和notify为什么必须在同步块中调用的原因,如果个人理解有问题,欢迎指正!

袁琼琼 wechat
欢迎您扫一扫上面的微信公众号,订阅我的博客!
多谢支持,共同成长!
0%