【小家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循环里,这样能够完全的避免问题。


相关文章
|
3月前
|
关系型数据库 MySQL Java
【IDEA】java后台操作mysql数据库驱动常见错误解决方案
【IDEA】java后台操作mysql数据库驱动常见错误解决方案
142 0
|
1月前
|
安全 Java 程序员
深入理解Java内存模型与并发编程####
本文旨在探讨Java内存模型(JMM)的复杂性及其对并发编程的影响,不同于传统的摘要形式,本文将以一个实际案例为引子,逐步揭示JMM的核心概念,包括原子性、可见性、有序性,以及这些特性在多线程环境下的具体表现。通过对比分析不同并发工具类的应用,如synchronized、volatile关键字、Lock接口及其实现等,本文将展示如何在实践中有效利用JMM来设计高效且安全的并发程序。最后,还将简要介绍Java 8及更高版本中引入的新特性,如StampedLock,以及它们如何进一步优化多线程编程模型。 ####
37 0
|
27天前
|
JSON 前端开发 Java
【Bug合集】——Java大小写引起传参失败,获取值为null的解决方案
类中成员变量命名问题引起传送json字符串,但是变量为null的情况做出解释,@Data注解(Spring自动生成的get和set方法)和@JsonProperty
|
2天前
|
JSON 前端开发 安全
【潜意识java】前后端跨域问题及解决方案
本文深入探讨了跨域问题及其解决方案。跨域是指浏览器出于安全考虑,限制从一个域加载的网页请求另一个域的资源。
17 0
|
2月前
|
设计模式 Java 开发者
Java多线程编程的陷阱与解决方案####
本文深入探讨了Java多线程编程中常见的问题及其解决策略。通过分析竞态条件、死锁、活锁等典型场景,并结合代码示例和实用技巧,帮助开发者有效避免这些陷阱,提升并发程序的稳定性和性能。 ####
|
2月前
|
缓存 Java 开发者
Java多线程并发编程:同步机制与实践应用
本文深入探讨Java多线程中的同步机制,分析了多线程并发带来的数据不一致等问题,详细介绍了`synchronized`关键字、`ReentrantLock`显式锁及`ReentrantReadWriteLock`读写锁的应用,结合代码示例展示了如何有效解决竞态条件,提升程序性能与稳定性。
244 6
|
2月前
|
安全 Java 开发者
Java多线程编程中的常见问题与解决方案
本文深入探讨了Java多线程编程中常见的问题,包括线程安全问题、死锁、竞态条件等,并提供了相应的解决策略。文章首先介绍了多线程的基础知识,随后详细分析了每个问题的产生原因和典型场景,最后提出了实用的解决方案,旨在帮助开发者提高多线程程序的稳定性和性能。
|
2月前
|
人工智能 监控 数据可视化
Java智慧工地信息管理平台源码 智慧工地信息化解决方案SaaS源码 支持二次开发
智慧工地系统是依托物联网、互联网、AI、可视化建立的大数据管理平台,是一种全新的管理模式,能够实现劳务管理、安全施工、绿色施工的智能化和互联网化。围绕施工现场管理的人、机、料、法、环五大维度,以及施工过程管理的进度、质量、安全三大体系为基础应用,实现全面高效的工程管理需求,满足工地多角色、多视角的有效监管,实现工程建设管理的降本增效,为监管平台提供数据支撑。
59 3
|
3月前
|
存储 消息中间件 安全
JUC组件实战:实现RRPC(Java与硬件通过MQTT的同步通信)
【10月更文挑战第9天】本文介绍了如何利用JUC组件实现Java服务与硬件通过MQTT的同步通信(RRPC)。通过模拟MQTT通信流程,使用`LinkedBlockingQueue`作为消息队列,详细讲解了消息发送、接收及响应的同步处理机制,包括任务超时处理和内存泄漏的预防措施。文中还提供了具体的类设计和方法实现,帮助理解同步通信的内部工作原理。
JUC组件实战:实现RRPC(Java与硬件通过MQTT的同步通信)
|
2月前
|
存储 缓存 安全
Java内存模型(JMM):深入理解并发编程的基石####
【10月更文挑战第29天】 本文作为一篇技术性文章,旨在深入探讨Java内存模型(JMM)的核心概念、工作原理及其在并发编程中的应用。我们将从JMM的基本定义出发,逐步剖析其如何通过happens-before原则、volatile关键字、synchronized关键字等机制,解决多线程环境下的数据可见性、原子性和有序性问题。不同于常规摘要的简述方式,本摘要将直接概述文章的核心内容,为读者提供一个清晰的学习路径。 ####
54 2