本文主要是介绍Java多线程之虚假唤醒(原创),希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!
Java多线程之虚假唤醒
文章目录
- Java多线程之虚假唤醒
- 虚假唤醒的定义
- 从`生产者-消费者`场景讲起
- 单生产者-单消费者场景
- 多生产者-多消费者场景
- 这就是虚假唤醒吗?
首先需要说明的是,虚假唤醒不是Java语言特有的问题,而是多线程通信特有的问题,在java中就体现在
sychronized-wait-notify
上,最典型的应用场景就是
生产者-消费者
模式。
在网上翻看了很多关于虚假唤醒
的文档,才发现大多数人说的都是错的。要么语焉不详,要么南辕北辙,不一而足。于是我决定自己写一篇文章来说一说:到底什么是虚假唤醒?
虚假唤醒的定义
无可避免的落入俗套,先放上虚假唤醒的定义。但我要说明的是,这个定义放在这儿你可以只是大致浏览一下,最终请读完整个文章之后再回来看这个定义,应该效果更佳。
A spurious wakeup happens when a thread wakes up from waiting on a condition variable that’s been signaled, only to discover that the condition it was waiting for isn’t satisfied. It’s called spurious because the thread has seemingly been awakened for no reason. But spurious wakeups don’t happen for no reason: they usually happen because, in between the time when the condition variable was signaled and when the waiting thread finally ran, another thread ran and changed the condition. There was a race condition between the threads, with the typical result that sometimes, the thread waking up on the condition variable runs first, winning the race, and sometimes it runs second, losing the race. ----from Wikipedia
当线程从等待状态中被唤醒时,只是发现未满足其正在等待的条件时,就会发生虚假唤醒。 之所以称其为虚假的,是因为该线程似乎无缘无故被唤醒。 虚假唤醒不会无缘无故发生,通常是因为在发起唤醒号和等待线程最终运行之间的临界时间内,线程不再满足竞态条件。
从生产者-消费者
场景讲起
生产者-消费者
是多线程中教程中最常用的教学场景,主要用来模拟进程间通信,映射在java语言上,最常用的语法就是进程间通信三件套sychronized-wait-notify
。
现在有一个这样的场景,某甜品店进行蛋糕的生产和销售。由于甜品的特殊性,要求甜品店里库存的甜品不能大于100,避免卖不出去浪费。
单生产者-单消费者场景
在这种要求下,我们来使用代码模拟一下。首先假设甜品店只有一个生产进程和一个销售进程。
甜品类:
import java.util.concurrent.TimeUnit;public class Cookie {// 甜品库存数目// 根据要求,这个值应该满足: 10 =< count <= 100private int count;public Cookie() {}public int getCount() {return count;}public void setCount(int count) {this.count = count;}public synchronized void create() {if (count >= 100) {try {System.out.println(Thread.currentThread().getName()+"被挂起,因为此时甜品库存已达到最高位100");this.wait();} catch (InterruptedException e) {e.printStackTrace();}}// 库存尚未达到最高位100count++;System.out.println(Thread.currentThread().getName()+"生产了一个甜品,当前甜品数目为:"+ count);this.notifyAll();}public synchronized void sale() {if (count <= 0) {try {TimeUnit.SECONDS.sleep(1);System.out.println(Thread.currentThread().getName()+"被挂起,因为此时已无甜品可卖。");this.wait();} catch (InterruptedException e) {e.printStackTrace();}}// 尚有甜品count--;System.out.println(Thread.currentThread().getName()+"出售了一个甜品,当前甜品数目为:"+ count);this.notifyAll();}
}
生产者类:
import java.util.concurrent.TimeUnit;public class Product implements Runnable {private Cookie cookie;public Product(Cookie cookie) {this.cookie = cookie;}@Overridepublic void run() {for (int i = 0; i < 20; i++) {try {TimeUnit.SECONDS.sleep(1);} catch (InterruptedException e) {e.printStackTrace();}cookie.create();}}
}
消费者类:
import java.util.concurrent.TimeUnit;public class Customer implements Runnable {private Cookie cookie;public Customer(Cookie cookie) {this.cookie = cookie;}@Overridepublic void run() {for (int i = 0; i < 20; i++) {try {TimeUnit.SECONDS.sleep(1);} catch (InterruptedException e) {e.printStackTrace();}cookie.sale();}}
}
主题逻辑类:
public class Main {public static void main(String[] args) {Cookie cookie = new Cookie();Runnable r1 = new Product(cookie);Runnable r2 = new Customer(cookie);Thread t1 = new Thread(r1, "生产者1号");Thread t2 = new Thread(r2, "消费者1号");t1.start();t2.start();}
}
有经验的读者可能已经发现了代码中的问题,这时作者故意预留的bug。
此时代码的输出为:
生产者1号生产了一个甜品,当前甜品数目为:1
消费者1号出售了一个甜品,当前甜品数目为:0
生产者1号生产了一个甜品,当前甜品数目为:1
消费者1号出售了一个甜品,当前甜品数目为:0
生产者1号生产了一个甜品,当前甜品数目为:1
消费者1号出售了一个甜品,当前甜品数目为:0
生产者1号生产了一个甜品,当前甜品数目为:1
消费者1号出售了一个甜品,当前甜品数目为:0
生产者1号生产了一个甜品,当前甜品数目为:1
消费者1号出售了一个甜品,当前甜品数目为:0
生产者1号生产了一个甜品,当前甜品数目为:1
消费者1号出售了一个甜品,当前甜品数目为:0
生产者1号生产了一个甜品,当前甜品数目为:1
消费者1号出售了一个甜品,当前甜品数目为:0
生产者1号生产了一个甜品,当前甜品数目为:1
消费者1号出售了一个甜品,当前甜品数目为:0
消费者1号被挂起,因为此时已无甜品可卖。
生产者1号生产了一个甜品,当前甜品数目为:1
消费者1号出售了一个甜品,当前甜品数目为:0
消费者1号被挂起,因为此时已无甜品可卖。
......
此时代码的运行是逻辑上是没有问题的,生产者生产一个,消费者就消费一个;当没有库存而消费者线程被唤醒时,则会被挂起。
但是这个代码是有严重设计缺陷的,在线程体进行条件判断时应该使用while
而非if
。之所以不出问题是因为生产者和消费者线程都只有一个。此时如果消费者线程被阻塞,则它只有等待生产者线程调用notifyAll
来唤醒它,而在唤醒它之前,生产者已经完成了生产操作,从而使得库存没有出现大于100或是小于0的情况。
多生产者-多消费者场景
我们只需更改逻辑代码为:
public class Main {public static void main(String[] args) {Cookie cookie = new Cookie();Runnable r1 = new Product(cookie);Runnable r2 = new Product(cookie);Runnable r3 = new Product(cookie);Runnable r4 = new Customer(cookie);Runnable r5 = new Customer(cookie);Runnable r6 = new Customer(cookie);Thread t1 = new Thread(r1, "生产者1号");Thread t2 = new Thread(r2, "生产者2号");Thread t3 = new Thread(r3, "生产者3号");Thread t4 = new Thread(r4, "消费者1号");Thread t5 = new Thread(r5, "消费者2号");Thread t6 = new Thread(r6, "消费者3号");t1.start();t2.start();t3.start();t4.start();t5.start();t6.start();}
}
此时输出的结果为:
消费者2号被挂起,因为此时已无甜品可卖。
消费者3号被挂起,因为此时已无甜品可卖。
消费者1号被挂起,因为此时已无甜品可卖。
生产者3号生产了一个甜品,当前甜品数目为:1
消费者2号出售了一个甜品,当前甜品数目为:0
消费者1号出售了一个甜品,当前甜品数目为:-1
消费者3号出售了一个甜品,当前甜品数目为:-2
生产者1号生产了一个甜品,当前甜品数目为:-1
生产者2号生产了一个甜品,当前甜品数目为:0
消费者3号被挂起,因为此时已无甜品可卖。
消费者2号被挂起,因为此时已无甜品可卖。
消费者1号被挂起,因为此时已无甜品可卖。
生产者3号生产了一个甜品,当前甜品数目为:1
消费者3号出售了一个甜品,当前甜品数目为:0
消费者1号出售了一个甜品,当前甜品数目为:-1
消费者2号出售了一个甜品,当前甜品数目为:-2
生产者1号生产了一个甜品,当前甜品数目为:-1
生产者2号生产了一个甜品,当前甜品数目为:0
消费者3号被挂起,因为此时已无甜品可卖。
消费者1号被挂起,因为此时已无甜品可卖。
消费者2号被挂起,因为此时已无甜品可卖。
......
可以看出此时代码逻辑出现了问题,库存竟然出现了负数。
那么问题来自哪里呢?问题就来自于我们使用了if
作为条件判断而不是while
来做循环条件判断。
在说明两者的区别之前,我们需要明白,当一个线程调用同步对象的wait
方法后,当前线程会:
- 释放CPU
- 释放对象锁
- 只有等待该同步对象调用
notify/notifyAll
该线程才会被唤醒,唤醒后继续从wait
处的下一行代码开始执行
这里最关键的就是唤醒后继续从wait处的下一行代码开始执行
,这意味着:
- 如果使用
if
,条件判断只进行一次,下次被唤醒的时候已经绕过了条件判断,从wait
后的语句开始顺序执行; - 如果使用
while
,wait
后的语句在循环体内,虽然绕过了上一次的条件判断,但终究会进入下一轮条件判断。
现在来分析上面例子出错的原因:
输出 | 解释 |
---|---|
消费者2号被挂起,因为此时已无甜品可卖。 消费者3号被挂起,因为此时已无甜品可卖。 消费者1号被挂起,因为此时已无甜品可卖。 | 此时甜品库存为0,三个线程去访问资源文件时,都依次被挂起。 |
生产者3号生产了一个甜品,当前甜品数目为:1 | 生产者3生产了一个甜品,此时甜品库存为1。当前线程调用notifyAll ,从waitSet 中唤醒线程 |
消费者2号出售了一个甜品,当前甜品数目为:0 | 消费者2号被唤醒,拿到CPU和对象锁,出售一个甜品,此时甜品库存为0。当前线程调用notifyAll ,从waitSet 中唤醒线程 |
消费者1号出售了一个甜品,当前甜品数目为:-1 | 问题来了:由于notifyAll 只能随机唤醒waitSet 中的线程,它将消费者1唤醒了。由于之前阻塞时已经执行过了if ,此处直接向下执行消费操作,没有在进行库存的条件判断。将库存从0变为了-1. |
… | … |
… | … |
… | … |
后续的所有问题就是这样导致的。这也就是为什么我们的条件判断应该用while
的原因。总结起来,导致错误的原因有:
notify/notifyAll
无法指定唤醒线程,只能从waitSet
中随机唤醒- 被唤醒的线程从
wait
语句下一行开始执行,导致绕过了if的条件判断
这就是虚假唤醒吗?
很多教程中将这种必须使用while
来替代if
的操作成为虚假唤醒,我认为这是不对的。使用if来进行判断是代码的逻辑错误
,而不是真正的虚假唤醒。
根据上面的代码,我们将if全部替换为while,调整后为的甜品类为:
import java.util.concurrent.TimeUnit;public class Cookie {// 甜品库存数目// 根据要求,这个值应该满足: 10 =< count <= 100private int count;public Cookie() {}public int getCount() {return count;}public void setCount(int count) {this.count = count;}public synchronized void create() {while (count >= 100) {try {System.out.println(Thread.currentThread().getName() + "被挂起,因为此时甜品库存已达到最高位100");this.wait();if (count >= 100) {System.out.println(Thread.currentThread().getName() + "被虚假唤醒了,因为此时没有满足它的执行条件count < 100.");}} catch (InterruptedException e) {e.printStackTrace();}}// 库存尚未达到最高位100count++;System.out.println(Thread.currentThread().getName() + "生产了一个甜品,当前甜品数目为:" + count);this.notifyAll();}public synchronized void sale() {while (count <= 0) {try {TimeUnit.SECONDS.sleep(1);System.out.println(Thread.currentThread().getName() + "被挂起,因为此时已无甜品可卖。");this.wait();if (count <= 0) {System.out.println(Thread.currentThread().getName() + "被虚假唤醒了,因为此时没有满足它的执行条件count > 0.");}} catch (InterruptedException e) {e.printStackTrace();}}// 尚有甜品count--;System.out.println(Thread.currentThread().getName() + "出售了一个甜品,当前甜品数目为:" + count);this.notifyAll();}
}
执行结果为:
消费者2号被挂起,因为此时已无甜品可卖。
消费者1号被挂起,因为此时已无甜品可卖。
消费者3号被挂起,因为此时已无甜品可卖。
生产者3号生产了一个甜品,当前甜品数目为:1
消费者2号出售了一个甜品,当前甜品数目为:0
消费者3号被虚假唤醒了,因为此时没有满足它的执行条件count > 0.
消费者3号被挂起,因为此时已无甜品可卖。
消费者1号被虚假唤醒了,因为此时没有满足它的执行条件count > 0.
消费者1号被挂起,因为此时已无甜品可卖。
生产者2号生产了一个甜品,当前甜品数目为:1
生产者1号生产了一个甜品,当前甜品数目为:2
消费者1号出售了一个甜品,当前甜品数目为:1
消费者3号出售了一个甜品,当前甜品数目为:0
消费者2号被挂起,因为此时已无甜品可卖。
消费者3号被挂起,因为此时已无甜品可卖。
消费者1号被挂起,因为此时已无甜品可卖。
生产者3号生产了一个甜品,当前甜品数目为:1
消费者2号出售了一个甜品,当前甜品数目为:0
消费者1号被虚假唤醒了,因为此时没有满足它的执行条件count > 0.
消费者1号被挂起,因为此时已无甜品可卖。
消费者3号被虚假唤醒了,因为此时没有满足它的执行条件count > 0.
消费者3号被挂起,因为此时已无甜品可卖。
生产者2号生产了一个甜品,当前甜品数目为:1
生产者1号生产了一个甜品,当前甜品数目为:2
消费者2号出售了一个甜品,当前甜品数目为:1
消费者3号出售了一个甜品,当前甜品数目为:0
上述输出的Line 6,7 和Line19,20,21,22向我们展示了什么才是真正的虚假唤醒。
以Line6,7为例,Line5中消费者2消费了一个甜品,此时甜品库存为0,然后消费者2调用notifyAll方法,唤醒的却是同为消费者的消费者线程3 。紧接着消费者线程3又因为不满足while循环而在此被阻塞放入waitSet。
这样的一个过程在wait-notify
机制下是无法避免的,因为notify是随机唤醒的。这导致上例中消费者线程3被唤醒,唤醒后的消费者线程3却又发现自己的执行条件并没有满足,从而在此进入阻塞。
现在让我们翻到文章的前面,再来看看虚假唤醒的定义:
当线程从等待状态中被唤醒时,只是发现未满足其正在等待的条件时,就会发生虚假唤醒。 之所以称其为虚假的,是因为该线程似乎无缘无故被唤醒。 虚假唤醒不会无缘无故发生,通常是因为在发起唤醒号和等待线程最终运行之间的临界时间内,线程不再满足竞态条件。
有没有觉得理解又加深了一点?
更多文章请关注作者同名公众号:Cratels学编程
这篇关于Java多线程之虚假唤醒(原创)的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!