小故事引入
- 由于条件不满足,小南不能继续进行计算 但小南如果一直占用着锁,其它人就得一直阻塞,效率太低
- 于是老王单开了一间休息室(调用 wait 方法),让小南到休息室(WaitSet)等着去了,但这时锁释放开,其它人可以由老王随机安排进屋
- 直到小M将烟送来,大叫一声 [ 你的烟到了 ] (调用 notify 方法)
- 小南于是可以离开休息室,重新进入竞争锁的队列
在上面的小故事中,我们有下面几点需要注意的:
- Owner 线程如果发现执行条件不足,可以调用 Monitor 对象对应的 Java 对象的 wait 方法,则它就会进入到 WaitSet 列表,线程状态变为 WAITING
- 位于 EntryList 和 WaitSet 列表的线程都处于阻塞状态,只是从Java层面的线程状态不同,二者都不占用 CPU 时间片
- BLOCKED 线程会在 Owner 线程释放锁的时候被唤醒,至于下一个 Owner 是谁,则由操作系统决定
- 如果当前的 Owner 线程调用了该 Monitor 对象对应的 Java 对象的 notify 时,就会随机唤醒一个位于 WaitSet 的线程进入 EnteryList 等待 CPU 分配时间片
注意:
- 还有一个有参的 wait 方法,它是有时间限制的进入 WaitSet 列表,对应Java层面的线程状态就是TIME_WAITING,时间一过就进入EntryList列表
- 还有一个 notifyAll 方法,它是唤醒位于 WaitSet 列表里面的所有线程,使他们都进入到EntryList列表
举个小例子:
@Slf4j(topic = "c.Test")
public class Test {
final static Object obj = new Object();
public static void main(String[] args) throws InterruptedException {
new Thread(() -> {
synchronized (obj) {
log.debug("执行....");
try {
obj.wait(); // 让线程在obj上一直等待下去
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug("其它代码....");
}
},"t1").start();
new Thread(() -> {
synchronized (obj) {
log.debug("执行....");
try {
obj.wait(); // 让线程在obj上一直等待下去
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug("其它代码....");
}
},"t2").start();
// 主线程两秒后执行
Thread.sleep(2000);
log.debug("唤醒 obj 上其它线程");
synchronized (obj) {
obj.notify(); // 唤醒obj上一个线程
// obj.notifyAll(); // 唤醒obj上所有等待线程
}
}
}
先来看看调用 obj.notify() 的运行结果:
可以看到唤醒了t1线程,但是程序没有结束,因为t2还在无时间限制的wait。再看看调用 obj.notifyAll() 的运行结果:
可以看到t1、t2被同时唤醒,并且程序正常退出
wait和sleep的区别
wait和sleep虽然都可以让线程进入阻塞,但是二者有着很多并且具有本质性的区别,主要可以从一下几个维度分析:
所属类
wait 方法是 Object 类的方法,所有的类都会具有;但是 sleep 方法是 Thread 类的方法。但是二者都是 native 本地方法,前者是 final 修饰的 后者是静态方法,如下图所示:
使用限制
- 使用 sleep 方法可以让当前线程进入睡眠状态,时间一到线程会继续运行下去,在任何时候都可以调用,只需要处理或抛出对应的 InterruptedException 异常即可:
- wait方法只能在同步代码块(synchronized代码块)里面调用,同样也需要处理或抛出 InterruptedException 异常。至于其原因可以参考wait-notify的工作原理。
使用效果
在线程中调用某对象的wait方法,会释放当前线程对该对象持有的锁,但是sleep则不会释放锁。并且sleep 会让出 CPU 执行时间且强制上下文切换,而 wait 则不一定,wait 后可能还是有机会重新竞争到锁继续执行的。
wait-notify机制的正确使用
steep one
@Slf4j(topic = "c.Test")
public class Test {
static final Object room = new Object();
static boolean hasCigarette = false;
public static void main(String[] args) throws InterruptedException {
new Thread(() -> {
synchronized (room) {
log.debug("有烟没?[{}]", hasCigarette);
if (!hasCigarette) {
log.debug("没烟,歇一会先");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
log.debug("有烟没?[{}]", hasCigarette);
if (hasCigarette) {
log.debug("可以开始干活了");
} else {
log.debug("活没有干成");
}
}
}, "小南").start();
for (int i = 0; i < 5; i++) {
new Thread(() -> {
synchronized (room) {
log.debug("可以开始干活了");
}
}, "其它人").start();
}
Thread.sleep(1000);
new Thread(() -> {
// 这里能不能加 synchronized (room)?
hasCigarette = true;
log.debug("烟到了噢!");
}, "送烟的").start();
}
}
运行结果:
可以看到,如果在同步代码块中使用sleep进入等待,一定阻塞其它线程,会影响到并发效率。为了解决这个问题,可以采用wait-notify机制,如steep two
steep two
修改一下两处代码:
if (!hasCigarette) {
log.debug("没烟,歇一会先");
try {
room.wait(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
synchronized (room) {
hasCigarette = true;
log.debug("烟到了噢!");
room.notify();
}
运行结果:
可以看到。没有再阻塞其它线程。但是,上面只是一个线程再等待条件,如果有两个线程了?
steep three
为了方便测试,将有时间的wait换成无时间的wait,并且加多一个需要条件的线程:
@Slf4j(topic = "c.Test")
public class Test {
static final Object room = new Object();
static boolean hasCigarette = false;
static boolean hasTakeout = false;
public static void main(String[] args) throws InterruptedException {
new Thread(() -> {
synchronized (room) {
log.debug("有烟没?[{}]", hasCigarette);
if (!hasCigarette) {
log.debug("没烟,歇一会先");
try {
room.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
log.debug("有烟没?[{}]", hasCigarette);
if (hasCigarette) {
log.debug("可以开始干活了");
} else {
log.debug("活没有干成");
}
}
}, "小南").start();
new Thread(() -> {
synchronized (room) {
log.debug("有面包没?[{}]", hasCigarette);
if (!hasCigarette) {
log.debug("没面包,歇一会先");
try {
room.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
log.debug("有面包没?[{}]", hasCigarette);
if (hasCigarette) {
log.debug("可以开始干活了");
} else {
log.debug("活没有干成");
}
}
}, "小女").start();
for (int i = 0; i < 5; i++) {
new Thread(() -> {
synchronized (room) {
log.debug("可以开始干活了");
}
}, "其它人").start();
}
Thread.sleep(1000);
new Thread(() -> {
synchronized (room) {
hasTakeout = true;
log.debug("面包到了噢!");
room.notify();
}
}, "送面包的").start();
}
}
可以看到,小南在等烟,小女在等面包,看一下运行结果:
可以看到,面包送到了,但是唤醒的却是需要烟的小南,反而需要面包的小女没有被唤醒,这其实就叫做虚假唤醒,即不能唤醒正确的线程。其解决办法也很简单,就是改用notifyAll即可,如steep four
steep four
修改为:
new Thread(() -> {
synchronized (room) {
hasTakeout = true;
log.debug("面包到了噢!");
room.notifyAll();
}
}, "送面包的").start();
但是新的问题又来了:用 notifyAll 仅解决某个线程的唤醒问题,但使用 if + wait 判断仅有一次机会,一旦条件不成立,就没有重新判断的机会了。一个简单的解决方法就是使用:用 while + wait,当条件不成立,再次 wait
steep five
将if修改为while判断:
while (!hasCigarette) {
log.debug("没烟,先歇会!");
try {
room.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
下面就是wait-notify机制正确的使用模板: