【小家java】JUC并发编程之:虚假唤醒(spurious wakeup)以及推荐的解决方案

简介: 【小家java】JUC并发编程之:虚假唤醒(spurious wakeup)以及推荐的解决方案

前言


本文主要讲述一个概念:虚假唤醒(spurious wakeup)。


在并发编程中,我们可能在实践中并没有遇到过,但是它确实存在,概率较低,但一旦出现,问题就非常的大。


比如我们给方法上锁,经常会使用到this.wait()的方式,但是此方法JDK官方在doc文档里已经给我们说明了:它是有可能出现虚假唤醒现象的,如下截图我是在JDK官方的doc文档截的

image.png


大致的意思如下:


线程也可以在不被通知、中断或超时的情况下唤醒,即所谓的虚假唤醒。虽然这在实践中很少发生,但是应用程序必须通过测试导致线程被唤醒的条件来防止这种情况,并且如果条件不满足则继续等待。换句话说,等待应该总是在循环中发生。

代码示例



现在通过代码的方式,来演示出什么叫做虚假唤醒,这样能够更好的理解

先构建三个类:店员 生产者 消费者


// 店员类:负责进货和售货
class Clerk{
  //TOTAL表示我的店最大可以容纳的总量
  private static final int TOTAL=1; //数字取1是为了放大问题 
  private int num=0; //店里当前的货物量
  public synchronized void get() { //店员进货  每次进货一个
    if(num >= TOTAL) {
      System.out.println("库存已满,无法进货");
      try {
        this.wait();
      } catch (InterruptedException e) {
        e.printStackTrace();
      }
    } else {
      System.out.println(Thread.currentThread().getName()+" : "+ (num++));
      this.notifyAll(); 
    }
  }
  public synchronized void sale() { //店员卖货 每次卖掉一个货
    if(num<=0) {
      System.out.println("库存已空,无法卖货");
      try {
        this.wait();
      } catch (InterruptedException e) {
        e.printStackTrace();
      }
    }else {
      System.out.println(Thread.currentThread().getName()+" : "+(num--));
      this.notifyAll();
    }
  }
}
// 生产者 可以有很多生产者卖货给这个店员
class Producer implements Runnable{
  private Clerk clerk;
  public Producer(Clerk clerk) {
    this.clerk=clerk;
  }
  @Override
  public void run() {
    for (int i = 0; i<20; i++) {
      try {
        Thread.sleep(200); //放大问题
      } catch (InterruptedException e) {
        e.printStackTrace();
      }
      clerk.get();
    }
  }
}
//消费者:可以很多消费者找店员买货
class Consumer implements Runnable{
  private Clerk clerk;
  public Consumer(Clerk clerk) {
    this.clerk=clerk;
  }
  @Override
  public void run() {
    for (int i = 0; i<20; i++) {
      clerk.sale();
    }
  }
}


先我们只用一个生产者,一个消费者试试:


public static void main(String[] args) {
    Clerk clerk=new Clerk();
    Producer producer=new Producer(clerk);
    Consumer consumer=new Consumer(clerk);
    new Thread(producer,"生产者A1").start();
    new Thread(consumer,"消费者B1").start();
  }

各位读者可以先看看代码,猜猜结果。


代码咋一看,其实真的没什么问题,但是因为我们通过sleep把问题放大了,所以我们运行一下,**竟然发现控制台一直都没有结束。**而我们这里是for20次循环,按理来说程序最终会终止,可情况恰恰相反。


问题分析



分析产生上面控制台一直都没有停的原因:


问题产生的根源是,由于生产者现象睡眠了200毫秒,因而可能产生的情况是最后消费者线程循环走完了然后就真的结束了,而生产者线程由于wait没有线程来唤醒,所以最终导致一直等待,因而程序不会结束,控制台就不终止。


解决方案:


一:把同步方法的else去掉了,那么无论最终哪个线程先走完,都会执行wait后面的方法,即它在结束前会唤醒等待的线程,因而这个线程最终也会完整的执行完,最后程序终止。

if(num<=0) {
      System.out.println("库存已空,无法卖货");
      try {
        this.wait();
      } catch (InterruptedException e) {
        e.printStackTrace();
      }
    }
    System.out.println(Thread.currentThread().getName()+" : "+(num--));
    this.notifyAll();
... //另外一个同步方法等其余代码省略


这种办法貌似解决了我们的问题,程序也正常的终止了。但是我们再加一组生产者、消费者试试:


public static void main(String[] args) {
    Clerk clerk=new Clerk();
    Producer producer=new Producer(clerk);
    Consumer consumer=new Consumer(clerk);
    Producer producer2=new Producer(clerk);
    Consumer consumer2=new Consumer(clerk);
    new Thread(producer,"生产者A1").start();
    new Thread(consumer,"消费者B1").start();
    new Thread(producer2,"生产者A2").start();
    new Thread(consumer2,"消费者B2").start();
}


运行,竟然发现出现了产品为负数的情况。这,就,尴尬了。肯定不合适。因为我们把esle放开了,所以每次都notifyAll()出现了虚假唤醒现象。


因为有可能num==0,然后两个消费者线程都wait,此时生产者执行num++后,在唤醒却是唤醒了所有等待的线程,此时这两个消费者线程抢占资源后立马执行wait之后的操作,即num–就会出现产品为负的情况。


为了再表面这种现象,我们就要使用JDK中DOC给我们推荐的方法了:wait()方法往往建议都使用在while循环里面,因此我们继续改进:


把if改成while即可:

while(num>=TOTAL) {}

这样我们再次运行,完美,没任何毛病。不管我们用多少个生产者、消费者,都没有问题了。


总结


按照官方JDK说的,虚假唤醒在wait的时候是有可能发生的,因此建议都放在while循环里,这样能够完全的避免问题。


相关文章
|
1月前
|
Java 编译器 开发者
深入理解Java内存模型(JMM)及其对并发编程的影响
【9月更文挑战第37天】在Java的世界里,内存模型是隐藏在代码背后的守护者,它默默地协调着多线程环境下的数据一致性和可见性问题。本文将揭开Java内存模型的神秘面纱,带领读者探索其对并发编程实践的深远影响。通过深入浅出的方式,我们将了解内存模型的基本概念、工作原理以及如何在实际开发中正确应用这些知识,确保程序的正确性和高效性。
|
1月前
|
关系型数据库 MySQL Java
【IDEA】java后台操作mysql数据库驱动常见错误解决方案
【IDEA】java后台操作mysql数据库驱动常见错误解决方案
67 0
|
2月前
|
安全 Java API
JAVA并发编程JUC包之CAS原理
在JDK 1.5之后,Java API引入了`java.util.concurrent`包(简称JUC包),提供了多种并发工具类,如原子类`AtomicXX`、线程池`Executors`、信号量`Semaphore`、阻塞队列等。这些工具类简化了并发编程的复杂度。原子类`Atomic`尤其重要,它提供了线程安全的变量更新方法,支持整型、长整型、布尔型、数组及对象属性的原子修改。结合`volatile`关键字,可以实现多线程环境下共享变量的安全修改。
|
11天前
|
人工智能 监控 数据可视化
Java智慧工地信息管理平台源码 智慧工地信息化解决方案SaaS源码 支持二次开发
智慧工地系统是依托物联网、互联网、AI、可视化建立的大数据管理平台,是一种全新的管理模式,能够实现劳务管理、安全施工、绿色施工的智能化和互联网化。围绕施工现场管理的人、机、料、法、环五大维度,以及施工过程管理的进度、质量、安全三大体系为基础应用,实现全面高效的工程管理需求,满足工地多角色、多视角的有效监管,实现工程建设管理的降本增效,为监管平台提供数据支撑。
27 3
|
1月前
|
存储 消息中间件 安全
JUC组件实战:实现RRPC(Java与硬件通过MQTT的同步通信)
【10月更文挑战第9天】本文介绍了如何利用JUC组件实现Java服务与硬件通过MQTT的同步通信(RRPC)。通过模拟MQTT通信流程,使用`LinkedBlockingQueue`作为消息队列,详细讲解了消息发送、接收及响应的同步处理机制,包括任务超时处理和内存泄漏的预防措施。文中还提供了具体的类设计和方法实现,帮助理解同步通信的内部工作原理。
JUC组件实战:实现RRPC(Java与硬件通过MQTT的同步通信)
|
16天前
|
存储 缓存 安全
Java内存模型(JMM):深入理解并发编程的基石####
【10月更文挑战第29天】 本文作为一篇技术性文章,旨在深入探讨Java内存模型(JMM)的核心概念、工作原理及其在并发编程中的应用。我们将从JMM的基本定义出发,逐步剖析其如何通过happens-before原则、volatile关键字、synchronized关键字等机制,解决多线程环境下的数据可见性、原子性和有序性问题。不同于常规摘要的简述方式,本摘要将直接概述文章的核心内容,为读者提供一个清晰的学习路径。 ####
35 2
|
17天前
|
Java API Apache
|
1月前
|
Java
短频快task的java解决方案
本文探讨了Java自带WorkStealingPool的缺陷,特别是在任务中断方面的不足。普通线程池在处理短频快任务时存在锁竞争问题,导致性能损耗。文章提出了一种基于任务窃取机制的优化方案,通过设计合理的窃取逻辑和减少性能损耗,实现了任务的高效执行和资源的充分利用。最后总结了不同场景下应选择的线程池类型。
|
1月前
|
小程序 Java
小程序访问java后台失败解决方案
小程序访问java后台失败解决方案
44 2
|
2月前
|
传感器 监控 数据可视化
【Java】智慧工地解决方案源码和所需关键技术
智慧工地解决方案是一种新的工程全生命周期管理理念。它通过使用各种传感器、数传终端等物联网手段获取工程施工过程信息,并上传到云平台,以保障数据安全。
81 7