JUC并发编程 --wait 和 sleep的区别 & 加锁对象的小建议 & wait notify 的正确姿势 & 虚假唤醒

Posted Z && Y

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了JUC并发编程 --wait 和 sleep的区别 & 加锁对象的小建议 & wait notify 的正确姿势 & 虚假唤醒相关的知识,希望对你有一定的参考价值。

1. wait 和 sleep的区别

  1. sleep 是 Thread 方法,而 wait 是 Object 的方法
  2. sleep 不需要强制和 synchronized 配合使用,但 wait 需要
    和 synchronized 一起用(只有获取到了锁才可以调用wait方法)。
  3. sleep 在睡眠的同时,不会释放对象锁的,但 wait 在等待的时候会释放对象锁。

2. 加锁对象的小建议

对于加锁的对象,建议加锁final关键字,,如果要保证锁的是同一个对象,则需要保证对象的引用地址不会变,而final关键字可以做到这一点


3. wait notify 的正确姿势

下面这些代码是做了删了,理解思路即可


3.1 step1

思考下面的解决方案好不好,为什么?

static final Object room = new Object();
static boolean hasCigarette = false;
static boolean hasTakeout = false;

new Thread(() -> {
 synchronized (room) {
 log.debug("有烟没?[{}]", hasCigarette);
 if (!hasCigarette) {
 log.debug("没烟,先歇会!");
 sleep(2);
 }
 log.debug("有烟没?[{}]", hasCigarette);
 if (hasCigarette) {
 log.debug("可以开始干活了");
 }
 }
}, "小南").start();
for (int i = 0; i < 5; i++) {
 new Thread(() -> {
 synchronized (room) {
 log.debug("可以开始干活了");
 }
 }, "其它人").start();
 }
sleep(1);
new Thread(() -> {
 // 这里能不能加 synchronized (room)?
 hasCigarette = true;
 log.debug("烟到了噢!");
}, "送烟的").start();

输出:

  • 其它干活的线程,都要一直阻塞,效率太低
  • 小南线程必须睡足 2s 后才能醒来,就算烟提前送到,也无法立刻醒来
    加了 synchronized (room) 后,就好比小南在里面反锁了门睡觉,烟根本没法送进门,main 没加。
  • synchronized 就好像 main 线程是翻窗户进来的
  • 解决方法,使用 wait - notify 机制

3.2 step2

思考下面的实现行吗,为什么?

new Thread(() -> {
 synchronized (room) {
 log.debug("有烟没?[{}]", hasCigarette);
 if (!hasCigarette) {
 log.debug("没烟,先歇会!");
 try {
 room.wait(2000);
 } catch (InterruptedException e) {
 e.printStackTrace();
 }
 }
 log.debug("有烟没?[{}]", hasCigarette);
 if (hasCigarette) {
 log.debug("可以开始干活了");
 }
 }
}, "小南").start();
for (int i = 0; i < 5; i++) {
 new Thread(() -> {
 synchronized (room) {
 log.debug("可以开始干活了");
 }
 }, "其它人").start();
}
sleep(1);
new Thread(() -> {
 synchronized (room) {
 hasCigarette = true;
 log.debug("烟到了噢!");
 room.notify();
 }
}, "送烟的").start();

输出:

  • 解决了其它干活的线程阻塞的问题
  • 但如果有其它线程也在等待条件呢?

3.3 step3

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) {
 Thread thread = Thread.currentThread();
 log.debug("外卖送到没?[{}]", hasTakeout);
 if (!hasTakeout) {
 log.debug("没外卖,先歇会!");
 try {
 room.wait();
 } catch (InterruptedException e) {
 e.printStackTrace();
 }
 }
 log.debug("外卖送到没?[{}]", hasTakeout);
 if (hasTakeout) {
 log.debug("可以开始干活了");
 } else {
 log.debug("没干成活...");
 }
 }
}, "小女").start();
sleep(1);
new Thread(() -> {
 synchronized (room) {
 hasTakeout = true;
 log.debug("外卖到了噢!");
 room.notify();
 }
}, "送外卖的").start();

输出:

  • notify 只能随机唤醒一个 WaitSet 中的线程,这时如果有其它线程也在等待,那么就可能唤醒不了正确的线程,称之为【虚假唤醒】
  • 解决方法,改为 notifyAll

3.4 step4

new Thread(() -> {
 synchronized (room) {
 hasTakeout = true;
 log.debug("外卖到了噢!");
 room.notifyAll();
 }
}, "送外卖的").start();

输出:

  • 用 notifyAll 仅解决某个线程的唤醒问题,但使用 if + wait 判断仅有一次机会,一旦条件不成立,就没有重新判断的机会了
  • 解决方法,用 while + wait,当条件不成立,再次 wait

3.5 step5

将 if 改为 while:

if (!hasCigarette) {
 log.debug("没烟,先歇会!");
 try {
 room.wait();
 } catch (InterruptedException e) {
 e.printStackTrace();
 }
}

改动后:

while (!hasCigarette) {
 log.debug("没烟,先歇会!");
 try {
 room.wait();
 } catch (InterruptedException e) {
 e.printStackTrace();
 }
}

输出:


3.6 wiat notify 正确使用公式

根据以上5步分析,可以得出wiat notify 正确使用公式:

synchronized(lock) {
 while(条件不成立) {
 lock.wait();
 }
 // 干活
}
//另一个线程
synchronized(lock) {
 lock.notifyAll();
}


以上是关于JUC并发编程 --wait 和 sleep的区别 & 加锁对象的小建议 & wait notify 的正确姿势 & 虚假唤醒的主要内容,如果未能解决你的问题,请参考以下文章

JUC 高并发编程

JUC 高并发编程

JUC 高并发编程

多线程JUC并发篇常见面试详解

Java基础学习多线程实战面试题形式

Java---JUC并发篇(多线程详细版)