Java多线程中的虚假唤醒和如何避免
先来看一个例子
一个卖面的面馆,有一个做面的厨师和一个吃面的食客,需要保证,厨师做一碗面,食客吃一碗面,不能一次性多做几碗面,更不能没有面的时候吃面;按照上述操作,进行十轮做面吃面的操作。
用代码说话
首先我们需要有一个资源类,里面包含面的数量,做面操作,吃面操作;
当面的数量为0时,厨师才做面,做完面,需要唤醒等待的食客,否则厨师需要等待食客吃完面才能做面;
当面的数量不为0时,食客才能吃面,吃完面需要唤醒正在等待的厨师,否则食客需要等待厨师做完面才能吃面;
然后在主类中,我们创建一个厨师线程进行10次做面,一个食客线程进行10次吃面;
代码如下:
package com.duoxiancheng.code;
/**
 * @user: code随笔
 */
class Noodles{
    //面的数量
    private int num = 0;
    //做面方法
    public synchronized void makeNoodles() throws InterruptedException {
        //如果面的数量不为0,则等待食客吃完面再做面
        if(num != 0){
            this.wait();
        }
        num++;
        System.out.println(Thread.currentThread().getName()+"做好了一份面,当前有"+num+"份面");
        //面做好后,唤醒食客来吃
        this.notifyAll();
    }
    //吃面方法
    public synchronized void eatNoodles() throws InterruptedException {
        //如果面的数量为0,则等待厨师做完面再吃面
        if(num == 0){
            this.wait();
        }
        num--;
        System.out.println(Thread.currentThread().getName()+"吃了一份面,当前有"+num+"份面");
        //吃完则唤醒厨师来做面
        this.notifyAll();
    }
}
public class Test {
    public static void main(String[] args) {
        Noodles noodles = new Noodles();
        new Thread(new Runnable(){
            @Override
            public void run() {
                try {
                    for (int i = 0; i < 10 ; i++) {
                        noodles.makeNoodles();
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },"厨师A").start();
        new Thread(new Runnable(){
            @Override
            public void run() {
                try {
                    for (int i = 0; i < 10 ; i++) {
                        noodles.eatNoodles();
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },"食客甲").start();
    }
}
输出如下:

可以见到是交替输出的;
如果有两个厨师,两个食客,都进行10次循环呢?
Noodles类的代码不用动,在主类中多创建两个线程即可,主类代码如下:
public class Test {
    public static void main(String[] args) {
        Noodles noodles = new Noodles();
        new Thread(new Runnable(){
            @Override
            public void run() {
                try {
                    for (int i = 0; i < 10 ; i++) {
                        noodles.makeNoodles();
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },"厨师A").start();
        new Thread(new Runnable(){
            @Override
            public void run() {
                try {
                    for (int i = 0; i < 10 ; i++) {
                        noodles.makeNoodles();
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },"厨师B").start();
        new Thread(new Runnable(){
            @Override
            public void run() {
                try {
                    for (int i = 0; i < 10 ; i++) {
                        noodles.eatNoodles();
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },"食客甲").start();
        new Thread(new Runnable(){
            @Override
            public void run() {
                try {
                    for (int i = 0; i < 10 ; i++) {
                        noodles.eatNoodles();
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },"食客乙").start();
    }
}
此时输出如下:

虚假唤醒
上面的问题就是"虚假唤醒"。
当我们只有一个厨师一个食客时,只能是厨师做面或者食客吃面,并没有其他情况;
但是当有两个厨师,两个食客时,就会出现下面的问题:
- 初始状态
  
- 厨师A得到操作权,发现面的数量为0,可以做面,面的份数+1,然后唤醒所有线程;
  
- 厨师B得到操作权,发现面的数量为1,不可以做面,执行wait操作;
  
- 厨师A得到操作权,发现面的数量为1,不可以做面,执行wait操作;
  
- 食客甲得到操作权,发现面的数量为1,可以吃面,吃完面后面的数量-1,并唤醒所有线程;

6.  此时厨师A得到操作权了,因为是从刚才阻塞的地方继续运行,就不用再判断面的数量是否为0了,所以直接面的数量+1,并唤醒其他线程;

7. 此时厨师B得到操作权了,因为是从刚才阻塞的地方继续运行,就不用再判断面的数量是否为0了,所以直接面的数量+1,并唤醒其他线程;

这便是虚假唤醒,还有其他的情况,读者可以尝试画画图分析分析。
解决方法
出现虚假唤醒的原因是从阻塞态到就绪态再到运行态没有进行判断,我们只需要让其每次得到操作权时都进行判断就可以了;
所以将
if(num != 0){
	this.wait();
}
改为
while(num != 0){
	this.wait();
}
将
if(num == 0){
	this.wait();
}
改为
while(num == 0){
	this.wait();
}
即可。
微信搜索:code随笔 欢迎关注乐于输出Java,算法等干货的技术公众号。
Java多线程中的虚假唤醒和如何避免的更多相关文章
- java 多线程—— 线程等待与唤醒
		java 多线程 目录: Java 多线程——基础知识 Java 多线程 —— synchronized关键字 java 多线程——一个定时调度的例子 java 多线程——quartz 定时调度的例子 ... 
- java 多线程中的wait方法的详解
		java多线程中的实现方式存在两种: 方式一:使用继承方式 例如: PersonTest extends Thread{ String name; public PersonTest(String n ... 
- Java多线程中的常用方法
		本文将带你讲诉Java多线程中的常用方法 Java多线程中的常用方法有如下几个 start,run,sleep,wait,notify,notifyAll,join,isAlive,current ... 
- java多线程中的三种特性
		java多线程中的三种特性 原子性(Atomicity) 原子性是指在一个操作中就是cpu不可以在中途暂停然后再调度,既不被中断操作,要不执行完成,要不就不执行. 如果一个操作时原子性的,那么多线程并 ... 
- java多线程中并发集合和同步集合有哪些?区别是什么?
		java多线程中并发集合和同步集合有哪些? hashmap 是非同步的,故在多线程中是线程不安全的,不过也可以使用 同步类来进行包装: 包装类Collections.synchronizedMap() ... 
- java多线程中最佳的实践方案是什么?
		java多线程中最佳的实践方案是什么? 给你的线程起个有意义的名字.这样可以方便找bug或追踪.OrderProcessor, QuoteProcessor or TradeProcessor 这种名 ... 
- Java多线程中的竞争条件、锁以及同步的概念
		竞争条件 1.竞争条件: 在java多线程中,当两个或以上的线程对同一个数据进行操作的时候,可能会产生“竞争条件”的现象.这种现象产生的根本原因是因为多个线程在对同一个数据进行操作,此时对该数据的操作 ... 
- Java多线程中的死锁
		Java多线程中的死锁 死锁产生的原因 线程死锁是指由两个以上的线程互相持有对方所需要的资源,导致线程处于等待状态,无法往前执行. 当线程进入对象的synchronized代码块时,便占有了资源,直到 ... 
- Java多线程中线程间的通信
		一.使用while方式来实现线程之间的通信 package com.ietree.multithread.sync; import java.util.ArrayList; import java.u ... 
随机推荐
- MySQL各版本connector net msi
			从其他博主那里扒来的! 链接:https://pan.baidu.com/s/1C1fYepBFKfxU0NJS0aRyJw 提取码:awsl 
- 神州笔记本电脑【K670D】安装 Ubuntu18.04 系列操作
			一.使用U盘安装 Ubuntu 前的处理如下: 进入BIOS将SSD设为启动首选项 -> F10保存退出 -> 用方向键高亮ubuntu启动项 -> 按e键进入编辑状态 -> ... 
- Java_垃圾回收机制(未掌握)
			垃圾回收机制 任何一种垃圾回收算法一般要做两件基本事情: 发现无用的对象(没有任何变量引用该对象) 回收无用对象占用的内存空间 垃圾回收相关算法: 引用计数法, 引用可达法 分代垃圾回收机制: 不同的 ... 
- 典型分布式系统分析:Dynamo
			本文是典型分布式系统分析系列的第四篇,主要介绍 Dynamo,一个在 Amazon 公司内部使用的去中心化的.高可用的分布式 key-value 存储系统. 在典型分布式系统分析系列的第一篇 MapR ... 
- leetcode103:permutations-ii
			题目描述 给出一组可能包含重复项的数字,返回该组数字的所有排列 例如: [1,1,2]的排列如下: [1,1,2],[1,2,1], [2,1,1]. Given a collection of nu ... 
- 在pgsql库用触发器自动触发PostgreSQL的存储过程,实现插入。
			需求:在对表A 执行 insert操作时,筛选符合条件的数据 insert到表B中,编写为存储过程(postgreSQL数据库) [筛选条件]:1. dd !="A" 或是 dd为 ... 
- fork函数拓展
			1.fork之后父子进程共享文件:文件引用计数的值改变,共享偏移. 在下面的例子中test.txt为parentchil.如果子进程没有睡眠,两个进程交叉执行,内容不可预测. 1 #include&l ... 
- 解决Ubuntu配置nginx出现的问题
			Ubuntu18.04配置nginx出现的各种错误 缺少pcre库 编译nginx 出现错误 安装pcre库,出现错误 手动编译安装pcre库 (1)下载并解压pcre库 wget https://f ... 
- 不要再说不会Spring了!Spring第一天,学会进大厂!
			工作及面试的过程中,作为Java开发,Spring环绕在我们的身边,很多人都是一知半解,本次将用14天时间,针对容器中注解.组件.源码进行解读,AOP概念进行全方面360°无死角介绍,SpringMV ... 
- python-基础入门-1
			Python的打印为 print,等价于c语言的printf 1 print "hello again" 就能打印出hello again,简简单单,就这么一句. 我用的vsc ... 
