目录
前言
再开始本文之前 , 先给大家看一张图 , 这是Object.wait()的源码介绍 , 翻译过来内容如下
使当前线程等待另一个线程调用此对象的notify()方法或notifyAll()方法. 换句话说 , 这个方法就像他只是调用 wait(0) 一样 .
当前线程必须拥有此对象的监视器。线程释放此监视器的所有权,并等待另一个线程通过调用notify方法或notifyAll方法通知等待此对象监视器的线程唤醒。然后,线程等待,直到它可以重新获得监视器的所有权并恢复执行。
注意红框圈住部分 , 说的是 中断和虚假环境也是可能的 , 所以这种方法应该是重在循环内 , 至于为什么说 请继续往下看.
一 . 使用if所引发的问题
1.1 虚假唤醒
虚假唤醒这种情况只会出现在多线程环境中,指的是在多线程环境下,多个线程等待在同一个条件上,等到条件满足时,所有等待的线程都被唤醒,
当一定的条件触发时会唤醒很多在阻塞态的线程 , 其中一部分线程从条件变量中苏醒过来时,发现等待的条件并没有满足 , 这便是虚假唤醒
1.2 虚假唤醒代码示例
我们在使用线程时,进行条件判断时,往往会先考虑使用if进行判断,在线程进行等待时就会出现不确定的结果。先来看看两个线程下的操作。
首先创建一个模拟业务类, 也是一个经典案例 "生产者&消费者"
public class MyService {
private int num = 1;
/**
* 模拟上架货物 , 始终保证有货
*/
@SneakyThrows
public synchronized void plus() {
// 当货物数量大于0 , 不缺货 , 不用补 , 使当前线程休眠
if (num > 0) {
System.out.println("num = " + num + " plus线程休眠");
// wait() 使当线程停在此处, 直到notify() 或者 notifyAll()被唤醒后 , 接着向下执行
this.wait();
System.out.println("num = " + num + " plus线程被唤醒");
}
// num 自加(补货
num++;
// 补货完毕 , 唤醒等待当前对象的其他线程前来竞争锁
this.notify();
System.out.println("当前num == " + num);
}
/**
* 模拟消费货物
*/
@SneakyThrows
public synchronized void sub() {
// 当货物数量num <= 0 时说明没货了 , 线程休眠
if (num <= 0) {
System.out.println("num = " + num + " sub线程休眠");
this.wait();
System.out.println("num = " + num + " sub线程被唤醒");
}
// num 自减
num--;
// 唤醒等待当前对象的其他线程来竞争锁
this.notify();
System.out.println("当前num == " + num);
}
}
然后分别创建补货/售货两个线程 , 用来模拟后续的动作
// 补货线程
public class PlusThread implements Runnable {
private MyService myService;
public PlusThread(MyService myService) {
this.myService = myService;
}
@Override
public void run() {
for (int i = 0; i < 10; i++) {
myService.plus();
}
}
}
// 售货线程
public class SubThread implements Runnable {
private MyService myService;
public SubThread(MyService myService) {
this.myService = myService;
}
@Override
public void run() {
for (int i = 0; i < 10; i++) {
myService.sub();
}
}
}
接下来创建一个测试类 , 用来模拟多线程下的生产/消费
/**
* @author hxk
* @version test: ThreadTest01.java, v 0.1 2022-06-30 10:13 hxk Exp $
*/
public class ThreadTest01 {
@SneakyThrows
public static void main(String[] args) {
// new 一个业务类
MyService myService = new MyService();
// 补货线程
PlusThread plusThread = new PlusThread(myService);
// 售货线程
SubThread subThread = new SubThread(myService);
// 然后一个补货 , 一个售货
Thread p = new Thread(plusThread);
p.start();
Thread s = new Thread(subThread);
s.start();
}
}
执行main方法,结果如下图 .
这样执行得到的结果是正常的 , 暂时也看不出问题 , 可是当我们在此基础之上 , 将售货的线程再增加一个 , 问题就暴露出来了 , main方法中追加一下代码 , 再去执行看下效果
Thread s = new Thread(subThread);
s.start();
连续执行几次后发现 , 其中几次执行结果出现了 负值 , 为了方便大家理解个中缘由 , 我给所有线程都加上名字 , 便于大家理解 .
然后这里重新执行 几次 , 得到错误的日志入下 , 我将图中的几个关键点标记了起来
首先是关键点1 , 这一步是属于正常唤醒 , num = 0 了 , p0线程需要重新补货, 所以p0分别执行了num++ 和 , notify() 唤醒等待此对象的其他线程
然后就到了关键点2 , 这时候售货线程s1被唤醒 , 对num进行num-- , 同样的再执行notify() , 来唤醒等待该对象的其他线程 ,
这时候最关键的关键点3 , 本应该被唤醒的是p0 , 结果却唤醒了s0 , 而s0被唤醒的位置至关重要 , 那就是wait()函数后面, 注意这时候 已经处在 If 条件内了 , 就不在进行判断了 , 而是继续向下执行下去了 , 再一次 num-- , 这便出现了最开始看到的负数, 也就是虚假唤醒(线程从条件变量中苏醒过来时,发现等待的条件并没有满足)
1.3 解决虚假唤醒
要想解决虚假唤醒这个问题 , 其实在wait()的源码备注中就有给出解决方案, 同时也有说明 中断和虚假环境也是可能的 , 所以这种方法应该是重在循环内 ,
改良后代码
public class MyService {
private int num = 1;
/**
* 模拟上架货物 , 始终保证有货
*/
@SneakyThrows
public synchronized void plus() {
// 当货物数量大于0 , 不缺货 , 不用补 , 使当前线程休眠 , 改良后 此处判断变为了 while
while (num > 0) {
System.out.println("num = " + num + ", " + Thread.currentThread().getName() + "线程休眠");
// wait() 使当线程停在此处, 直到notify() 或者 notifyAll()被唤醒后 , 接着向下执行
this.wait();
System.out.println("num = " + num + ", " + Thread.currentThread().getName() + "线程被唤醒");
}
// num 自加(补货
num++;
// 补货完毕 , 唤醒等待当前对象的其他线程前来竞争锁
this.notify();
System.out.println("当前num == " + num);
}
/**
* 模拟消费货物
*/
@SneakyThrows
public synchronized void sub() {
// 当货物数量num <= 0 时说明没货了 , 线程休眠 改良后 此处判断变为了 while
while (num <= 0) {
System.out.println("num = " + num + ", " + Thread.currentThread().getName() + "线程休眠");
this.wait();
System.out.println("num = " + num + ", " + Thread.currentThread().getName() + "线程被唤醒");
}
// num 自减
num--;
// 唤醒等待当前对象的其他线程来竞争锁
this.notify();
System.out.println("当前num == " + num);
二 . 为什么用while就能解决问题
首先我们先来回顾一下java 基础 里面 if 和while 的区别 ,
结论
一个被唤醒的线程就处于就绪状态了,就可以等待被cpu调度了,
如果用if判断的话,唤醒后线程会从wait之后的代码开始运行,但是不会重新判断if条件,直接继续运行if代码块之后的代码.
而使用while虽然也会从wait之后的代码开始运行,但是唤醒后会重新判断循环条件,如果不成立再执行while代码块之后的代码块,成立的话继续wait。
所以必须用while来检查,这样可以保证每次被唤醒都会检查一次条件。