Java多线程之虚假唤醒(原创)

2024-04-23 10:48

本文主要是介绍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后的语句开始顺序执行;
  • 如果使用whilewait后的语句在循环体内,虽然绕过了上一次的条件判断,但终究会进入下一轮条件判断。

现在来分析上面例子出错的原因:

输出解释
消费者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多线程之虚假唤醒(原创)的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/928584

相关文章

JVM 的类初始化机制

前言 当你在 Java 程序中new对象时,有没有考虑过 JVM 是如何把静态的字节码(byte code)转化为运行时对象的呢,这个问题看似简单,但清楚的同学相信也不会太多,这篇文章首先介绍 JVM 类初始化的机制,然后给出几个易出错的实例来分析,帮助大家更好理解这个知识点。 JVM 将字节码转化为运行时对象分为三个阶段,分别是:loading 、Linking、initialization

Spring Security 基于表达式的权限控制

前言 spring security 3.0已经可以使用spring el表达式来控制授权,允许在表达式中使用复杂的布尔逻辑来控制访问的权限。 常见的表达式 Spring Security可用表达式对象的基类是SecurityExpressionRoot。 表达式描述hasRole([role])用户拥有制定的角色时返回true (Spring security默认会带有ROLE_前缀),去

浅析Spring Security认证过程

类图 为了方便理解Spring Security认证流程,特意画了如下的类图,包含相关的核心认证类 概述 核心验证器 AuthenticationManager 该对象提供了认证方法的入口,接收一个Authentiaton对象作为参数; public interface AuthenticationManager {Authentication authenticate(Authenti

Spring Security--Architecture Overview

1 核心组件 这一节主要介绍一些在Spring Security中常见且核心的Java类,它们之间的依赖,构建起了整个框架。想要理解整个架构,最起码得对这些类眼熟。 1.1 SecurityContextHolder SecurityContextHolder用于存储安全上下文(security context)的信息。当前操作的用户是谁,该用户是否已经被认证,他拥有哪些角色权限…这些都被保

Spring Security基于数据库验证流程详解

Spring Security 校验流程图 相关解释说明(认真看哦) AbstractAuthenticationProcessingFilter 抽象类 /*** 调用 #requiresAuthentication(HttpServletRequest, HttpServletResponse) 决定是否需要进行验证操作。* 如果需要验证,则会调用 #attemptAuthentica

Spring Security 从入门到进阶系列教程

Spring Security 入门系列 《保护 Web 应用的安全》 《Spring-Security-入门(一):登录与退出》 《Spring-Security-入门(二):基于数据库验证》 《Spring-Security-入门(三):密码加密》 《Spring-Security-入门(四):自定义-Filter》 《Spring-Security-入门(五):在 Sprin

Java架构师知识体认识

源码分析 常用设计模式 Proxy代理模式Factory工厂模式Singleton单例模式Delegate委派模式Strategy策略模式Prototype原型模式Template模板模式 Spring5 beans 接口实例化代理Bean操作 Context Ioc容器设计原理及高级特性Aop设计原理Factorybean与Beanfactory Transaction 声明式事物

Java进阶13讲__第12讲_1/2

多线程、线程池 1.  线程概念 1.1  什么是线程 1.2  线程的好处 2.   创建线程的三种方式 注意事项 2.1  继承Thread类 2.1.1 认识  2.1.2  编码实现  package cn.hdc.oop10.Thread;import org.slf4j.Logger;import org.slf4j.LoggerFactory

JAVA智听未来一站式有声阅读平台听书系统小程序源码

智听未来,一站式有声阅读平台听书系统 🌟&nbsp;开篇:遇见未来,从“智听”开始 在这个快节奏的时代,你是否渴望在忙碌的间隙,找到一片属于自己的宁静角落?是否梦想着能随时随地,沉浸在知识的海洋,或是故事的奇幻世界里?今天,就让我带你一起探索“智听未来”——这一站式有声阅读平台听书系统,它正悄悄改变着我们的阅读方式,让未来触手可及! 📚&nbsp;第一站:海量资源,应有尽有 走进“智听

在cscode中通过maven创建java项目

在cscode中创建java项目 可以通过博客完成maven的导入 建立maven项目 使用快捷键 Ctrl + Shift + P 建立一个 Maven 项目 1 Ctrl + Shift + P 打开输入框2 输入 "> java create"3 选择 maven4 选择 No Archetype5 输入 域名6 输入项目名称7 建立一个文件目录存放项目,文件名一般为项目名8 确定