【多线程-从零开始-陆】wait、notify和notifyAll

news2024/9/22 21:33:59

线程饿死

一个或多个线程因为无法获得执行所需的资源(如CPU时间、锁、或其他同步控制)而被长时间阻塞或延迟执行的情况。尽管这些线程可能处于可执行状态并且已经准备好运行,但由于资源分配的不均衡或调度策略的问题,它们无法获得执行的机会。

例子

你去 ATM 机取钱,在你后面还排了很对人。你进去后,门锁上了,但你发现 ATM 机里面没钱了,于是你出去了。
按理来说,你出来之后,应该轮到排在你后面的人进去了,可你刚把锁打开,前脚刚迈出去,却心想“里面会不会又有钱了?”,于是你又进去了,把门又锁上了,可你进去之后发现还是没钱。
于是你就这样反反复复、进进出出、开锁上锁。虽然你的行为没有造成任何死锁,但你后面的人却做不了任何事(其他线程无法执行任何逻辑),这就叫“线程饿死

  • 这属于一个概率性问题,和调度器具体的策略直接相关
  • 针对上述问题,同样可以使用 wait / notify 来解决
  • 让你在拿到锁的时候进行判定,判定当前是否执行“取钱”操作,如果能执行,就正常执行;如果不能执行,就需要主动释放锁,并且“阻塞等待”(通过调用 wait),此时这个线程就不会在后续参与锁的竞争了
  • 一直阻塞到“取钱”的条件具备了,此时再由其他线程通过通知机制(notify)唤醒这个线程

wait

因为线程在操作系统上的调度是随机的,而我们不喜欢随机,喜欢确定,所以增加了一些手段来让调度变得确定

  • 多个线程,需要控制线程之间执行某个逻辑的先后顺序,就可以让后执行的逻辑使用 wait,先执行的线程完成某些逻辑后,通过 notify 唤醒对应的 wait
  • 另外通过 waitnotify 也是为了解决“线程饿死”问题

当我们尝试使用 wait:

public class Demo3 {  
    public static void main(String[] args) throws InterruptedException {  
        Object obj = new Object();  
        System.out.println("wait 之前");  
        obj.wait();  
        System.out.println("wait 之后");  
    }
}

运行后,报错了image.png|574

  • Monitor:此处指的是 synchronized 这里的锁
  • 合起来为:非法的锁状态异常(加锁状态/未加锁状态)

  • 之所以会出现这种情况,是因为 wait 中会进行一个操作,就是针对 obj 对象,先进行解锁。所以,使用 wait 的时候,务必要放到 synchronized 代码块里面(必须得先加上锁,才能谈“解锁”)
  • 你进入 ATM 机后,发现没钱,就要“阻塞等待”,此时一定是你先解锁,再开门出去。就是先释放锁,再等待。如果你抱着锁等待,就也没把几回让给别人,因为别人也无法进去

  • 并且释放锁和加上锁这两个操作是通过 wait 同时来进行(打包成原子),若不是同时执行,那就可能发生线程切换
  • 比如在释放锁之后,插进来了一个通知正在等待代的线程继续执行操作的线程,可是前面那个才刚刚释放锁,还没开始进行执行等待的操作,最终这个线程由于错过了通知,将持续等待下去

public class Demo3 {  

    public static void main(String[] args) throws InterruptedException {  
        Object obj = new Object();  
        System.out.println("wait 之前");  
        synchronized(obj){  
            obj.wait();  
        }        
        System.out.println("wait 之后");  
    }
}
//打印结果为:wait 之前
  • 因为代码阻塞在中间了,所以后面的逻辑就无法完成,此时线程的状态变成了 WAITING(没有超时时间的等待,有超时时间的事 TIMED_WAITING
  • 并且由于代码中没有 notify,所以 wait 将一直持续等待下去 image.png|429

综上

  • wait 使调用的线程进入阻塞
  • wait 做三件事
    1. 释放锁
    2. 进入阻塞状态,准备接受通知
    3. 收到通知后,唤醒,并且重新尝试获取锁

注意

  • wait 默认是“死等”,但它还提供了一个带参数的版本,指定超时时间。若 wait 达到了最大时间,还没等到 notify,就不会继续等待了,而是继续执行
  • wait(1000)sleep(1000) 还是有本质区别的
    • 使用 wait 的本质目的是为了提前唤醒,而 sleep 就是固定时间的阻塞,不涉及唤醒(虽然 sleep 可以被 Interrupt 唤醒,但这是一个终止线程的操作,而不是唤醒)
    • wait 必须要搭配 synchronized 使用,并且 wait 会先释放锁,同时进行等待
    • sleep 和锁无关,如果不加锁,sleep 可以正常使用;如果加了锁,sleep 操作不会释放锁,会“抱着锁”,一起睡,其他线程无法拿到锁

notify唤醒wait的操作

public class Demo5 {  
    private static Object locker = new Object();  
  
    public static void main(String[] args) {  
        Thread t1 = new Thread(() -> {  
            synchronized (locker){  
                System.out.println("t1 wait之前");  
                try {  
                    locker.wait();  
                } catch (InterruptedException e) {  
                    throw new RuntimeException(e);  
                }                System.out.println("t1 wait之后");  
            }        
        });        
        Thread t2 = new Thread(() -> {  
            System.out.println("t2 notify之前");  
            Scanner scanner = new Scanner(System.in);  
            scanner.next(); //此处是通过这个next构造一个“阻塞”的状态  
  
            synchronized (locker) {  
                locker.notify();  
            }            
            System.out.println("t2 notify之后");  
        });        
        t1.start();  
        t2.start();  
    }
}
//运行结果:
t1 wait之前
t2 notify之前
9
t2 notify之后
t1 wait之后
  • 要保证加锁的对象和调用 wait 的对象是一样的,如果不是同一个对象,那么无法使用,因为锁的状态是不对的
  • 要确保调用 waitnotify 的对象是一样的才能唤醒
  • waitnotify 调用前都要加上锁
    • 因为在多线程中,一个线程加锁,一个不加,是无意义的,不会有任何的阻塞效果。此处希望 t2 执行 notify 的时候,t1 是未持有锁的状态,如果 t1 正持有锁,肯定就不是在 wait,这个时候去 notify 也没有意义。所以要确保 notify 拿到锁,再去 notify 唤醒 wait
  • 一定要确保持有锁才能谈释放
    • 假设是多个现场,如果多个线程都在同一个对象上 waitnotify 只会随机唤醒其中一个

notifyAll

notify 相对,还有一个 notifyAll

将上面的notify换成notifyAll之后
运行结果为:
---
t1 wait之前
t3 wait之前
t2 wait之前
9
t4 notifyAll之前
t4 notifyAll之后
t1 wait之后
t2 wait之后
t3 wait之后
  • 大部分情况下,都是使用 notify。若要唤醒多个,就一个一个地唤醒,整个程序执行过程是比较有序的,如果一下全唤醒,这些被唤醒的线程就会无序的竞争锁(会很混乱,可能带来未知的风险)

  • notifynotifyAll 通知的时候,如果没有线程在 wait,不会有任何副作用

练习

创建三个线程,使用 waitnotify 控制先后打印 A、B、C 三个字母

public class Demo6 {  
    private static Object locker = new Object();  
    private static Object locker2 = new Object();  
  
    public static void main(String[] args) {  
        Thread t1 = new Thread(() -> {  
            System.out.println("A");  
            //Thread.sleep(1000);
            synchronized (locker) {  
                locker.notify();  //这个notify用来唤醒t2中的wait,因为是locker锁
            }        
        });        
        Thread t2 = new Thread(() -> {  
            synchronized (locker) {  
                try {  
                    locker.wait();  //被t1的notify唤醒
                } catch (InterruptedException e) {  
                    throw new RuntimeException(e);  
                }                
                System.out.println("B");  
  
                synchronized (locker2){  
                    locker2.notify();  //这个notify用来唤醒t3中的wait,因为是locker2锁
                }            
            }        
        });        
        Thread t3 = new Thread(() -> {  
            synchronized (locker2) {  
                try {  
                    locker2.wait();  //被t2的notify唤醒
                } catch (InterruptedException e) {  
                    throw new RuntimeException(e);  
                }            
            }            
            System.out.println("C");  
        });        
        t1.start();  
        t2.start();  
        t3.start();  
    }
}
  • 这个代码中,若是先执行 t2wait,后执行 t1notify,代码逻辑就一切顺利(大概率就是这样,因为 t1 的打印需要不少时间)
  • 但存在这样的可能:t1 限制性了打印和 notify,然后 t2 才执行 wait,意味着通知来早了,t2 错过了通知,t2wait 就无人唤醒了
  • 为了解决这样的情况,我们只需要在 t1 里面加一个 sleep 就可以了 (只要在锁的前面就行),让 t1 线程等一会,其他线程先执行

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1990093.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

快讯 | 单卡4090显卡即可解锁视频生成,智谱AI CogVideoX模型开源!

在数字化浪潮的推动下,人工智能(AI)正成为塑造未来的关键力量。硅纪元视角栏目紧跟AI科技的最新发展,捕捉行业动态;提供深入的新闻解读,助您洞悉技术背后的逻辑;汇聚行业专家的见解,…

认识RAID磁盘阵列

文章目录 1、RAID磁盘阵列介绍2、多种RAID级别RAID 0 (条带化存储)RAID 1 (镜像存储)RAID 5RAID 6RAID 10(先做镜象,再做条带)RAID 01(先做条带,再做镜象)区别…

leetcode日记(64)最小覆盖子串

很复杂的题目,无论是思路还是实践都很难… 思路还是看了答案(?)设定两个指针“框”出一串字符串,初始两个指针都指在s的零位,先移动下指针,直到使框出的字符串中包含t中所有字符串,…

c# 排序、强转枚举

List<Tuple<double,int>> mm中doble从小到大排序 mm本身排序 在C#中&#xff0c;如果你有一个List<Tuple<double, int>>类型的集合mm&#xff0c;并且你想要根据Tuple中的double值&#xff08;即第一个元素&#xff09;从小到大进行排序&#xff0c;同…

TCP全队列连接,tcpdum抓包

TCP全队列连接&#xff0c;tcpdum抓包 1. listen的第二个参数作用2. 理解全连接队列&#xff08;原理&#xff09;3. 为什么要有全连接队列并且队列长度要适当4. 使用不tcpdump 进行抓包&#xff0c;分析TCP过程&#xff08;三次握手&#xff0c;四次挥手&#xff09;4.1安装tc…

CVE-2023-28525~文件上传【春秋云境靶场渗透】

# 今天我们来攻克CVE-2023-28525文件上传漏洞# 看到页面&#xff0c;经过尝试 账号&#xff1a;admin 密码&#xff1a;admin# 发现Posts可以添加文件上传# 尝试发现&#xff0c;只能上传图片格式的文件&#xff0c;那我们试一下看能不能上传成功# 发现上传失败&#xff0c;发现…

console与控制台使用demo

文章目录 console是一个普通(实例)对象A、函数对象拥有2个属性&#xff1a;B、console只有__proto__属性&#xff1a;C、综上&#xff0c;console是一个实例对象 console拥有的函数&#xff08;特殊列举&#xff09;占位符用法以参数为key记录执行次数——count()dir()和dirxml…

Cmake基础教程--第1章:初识cmake

Cmake基础教程--第1章&#xff1a;初识cmake 何为CmakeCmake的优缺点优点缺点 Cmake与makefile之间的关系C/C 如何编译为可执行文件一个最简单的Cmake示例 从本篇文章开始&#xff0c;我会出一系列文章&#xff0c;致力于Cmake工具的学习使用。阅读本文章之前&#xff0c; 作者…

#MFC 编译错误msvcrt.lib(exe_main.obj) : error LNK2019

如果是CMake&#xff0c;需要改如下&#xff1a; set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} /SUBSYSTEM:CONSOLE") 改为 set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} /SUBSYSTEM:WINDOWS") 如果是mfc工程文件&#xff0c;需…

【数据结构】五、树:7.哈夫曼树、哈夫曼编码

3.哈夫曼树和哈夫曼编码 文章目录 3.哈夫曼树和哈夫曼编码3.1带权路径长度3.2哈夫曼树的定义和原理3.3哈夫曼树的构造代码实现 3.4特点3.5哈夫曼编码压缩比代码实现 3.6哈夫曼树-C 3.1带权路径长度 #mermaid-svg-yeVKyVnDwvdIc5ML {font-family:"trebuchet ms",verda…

【第18章】Spring Cloud之Gateway配置

文章目录 前言一、示例二、Route Metadata Configuration1. 路由元数据配置2. 获取元数据配置 三、Http timeouts configuration(请求超时配置)1. Global timeouts(全局)2. Per-route timeouts(路由) 四、CORS Configuration(跨域配置)1. Global CORS Configuration(全局)2. Ro…

【HarmonyOS NEXT星河版开发学习】小型测试案例07-弹性布局小练习

个人主页→VON 收录专栏→鸿蒙开发小型案例总结​​​​​ 基础语法部分会发布于github 和 gitee上面&#xff08;暂未发布&#xff09; 前言 在鸿蒙&#xff08;HarmonyOS&#xff09;开发中&#xff0c;Flex布局是一种非常有用的布局方式&#xff0c;它允许开发者创建灵活且响…

FPGA知识基础之--存储器知识点总结以及基于ip核的简单双端口RAM的实现和仿真(附RTL代码和Testbench代码)

目录 前言一、存储器的分类二、实验任务三 、简单&#xff08;伪&#xff09;双端口四、程序设计4.1 模块4.2 时序分析4.3 RTL代码ram_wr 写模块2.ram_rd 写模块3.top模块 五、仿真 前言 笔者在最近的存储器学习时&#xff0c;遇到了一些问题&#xff0c;为此笔者用本篇博客来…

ICM-20948芯片详解(9)

接前一篇文章&#xff1a;ICM-20948芯片详解&#xff08;8&#xff09; 六、寄存器详解 2. USER BANK 0寄存器详述 &#xff08;6&#xff09;INT_PIN_CFG 参考代码&#xff1a; #define ICM20948_INT_PIN_CFG 0x0F &#xff08;7&#xff09;INT_ENABLE 参考代码&#x…

我有10台120kw的直流充电桩,赢利了多少钱?我列出所有成本和多少利润,这里要算上政府补贴。【慧哥开源充电桩平台 HZCOS-chargeOS-cloud】

特别申明&#xff1a;仅仅代表个人观点&#xff0c;错的地方虚心请教学习&#xff0c;各位手下留情 拥有的10台120kW直流充电桩的盈利情况&#xff0c;并考虑到政府补贴&#xff0c;具体数字需要根据实际情况调整。 成本计算 初始投资成本 充电桩成本&#xff1a;每台120kW直…

利用Llama 3 API实现盈利:细节解析

随着人工智能技术的快速发展,基于大模型的服务成为了众多初创企业关注的焦点。Llama 3 API作为一种强大的语言模型接口,为小型公司提供了利用先进AI技术的机会。本文将探讨这些小公司如何通过Llama 3 API实现盈利,并分析其中的关键因素。 一、Llama 3 API性能概览 批处理输…

网络药理学:分子对接之一:macos上MOE和Autodock和PyMol和gromacs的下载、PDB数据库使用、gromacs能量最小化

MOE下载 别想了&#xff0c;要钱的。而且不算是主流软件&#xff0c;过。 Autodock和Autodock tools下载 下载地址&#xff1a;https://autodock.scripps.edu/download-autodock4/ 如果你的电脑满足以下配置&#xff0c;那么推荐下载autodock GPU 操作系统&#xff1a;mac…

重磅!观测云荣获SOC 2 Type II鉴证报告

近日&#xff0c;观测云在数据安全和内控管理领域再获殊荣&#xff0c;成功获得全球四大会计师事务所之一的安永会计师事务所签发的SOC 2 Type II 鉴证报告。这一荣誉不仅是对观测云在相关领域卓越表现的认可&#xff0c;更是对其对客户承诺坚定性和执行力的有力证明。 观测云 …

软件设计之JavaScript(1)

软件设计之JavaScript(1) 【狂神说Java】JavaScript最新教程通俗易懂 学习内容&#xff1a; 软件开发技能点参照&#xff1a;软件开发&#xff0c;小白变大佬&#xff0c;这套学习路线让你少走弯路是认真的&#xff0c;欢迎讨论 软件开发技能点顺序参照&#xff1a;Java学习…

“前缀和”专题篇一

目录 【模版】前缀和 【模版】二维前缀和 寻找数组的中心下标 除自身以外数组的乘积 【模版】前缀和 题目 思路 这道题如果使用暴力解法&#xff0c;即针对每次查询&#xff0c;先算出前r个数的总和&#xff0c;然后再算出前l-1个数的总和&#xff0c;然后相减就得出本次查…