【多线程】volatile 关键字、wait 和 notify方法详解

volatile 、wait 和 notify

  • 🌲volatile关键字
    • 🚩保证内存可见性
    • 🚩volatile 不保证原⼦性
  • 🌳wait 和 notify方法
    • 🚩wait()
    • 🚩notify()
    • 🚩notifyAll()方法
  • ⭕wait 和 sleep 的对比( 面试题)

🌲volatile关键字

🚩保证内存可见性

volatile 修饰的变量, 能够保证 “内存可⻅性”.
在这里插入图片描述
代码在写⼊ volatile 修饰的变量的时候,

• 改变线程⼯作内存中volatile变量副本的值
• 将改变后的副本的值从⼯作内存刷新到主内存

代码在读取 volatile 修饰的变量的时候,

• 从主内存中读取volatile变量的最新值到线程的⼯作内存中
• 从⼯作内存中读取volatile变量的副本

前⾯我们讨论内存可⻅性时说了, 直接访问⼯作内存(实际是 CPU 的寄存器或者 CPU 的缓存), 速度⾮
常快, 但是可能出现数据不⼀致的情况.
加上 volatile , 强制读写内存. 速度是慢了, 但是数据变的更准确了.

代码示例

在这个代码中
• 创建两个线程 t1 和 t2
• t1 中包含⼀个循环, 这个循环以 flag == 0 为循环条件.
• t2 中从键盘读⼊⼀个整数, 并把这个整数赋值给 flag.
• 预期当⽤⼾输⼊⾮ 0 的值的时候, t1 线程结束.

static class Counter {
 public int flag = 0;
}
public static void main(String[] args) {
 Counter counter = new Counter();
 Thread t1 = new Thread(() -> {
 while (counter.flag == 0) {
 // do nothing
 }
 System.out.println("循环结束!");
 });
 Thread t2 = new Thread(() -> {
 Scanner scanner = new Scanner(System.in);
 System.out.println("输⼊⼀个整数:");
 counter.flag = scanner.nextInt();
 });
 t1.start();
 t2.start();
}
// 执⾏效果
// 当⽤⼾输⼊⾮0值时, t1 线程循环不会结束. (这显然是⼀个 bug)

t1 读的是⾃⼰⼯作内存中的内容.
当 t2 对 flag 变量进⾏修改, 此时 t1 感知不到 flag 的变化.

如果给 flag 加上 volatile

static class Counter {
 public volatile int flag = 0;
}
// 执⾏效果
// 当⽤⼾输⼊⾮0值时, t1 线程循环能够⽴即结束.

🚩volatile 不保证原⼦性

volatile 和 synchronized 有着本质的区别. synchronized 能够保证原⼦性, volatile 保证的是内存可⻅
性.

代码⽰例
这个是最初的演⽰线程安全的代码.
• 给 increase ⽅法去掉 synchronized
• 给 count 加上 volatile 关键字.

static class Counter {
 volatile public int count = 0;
 void increase() {
 count++;
 }
}
public static void main(String[] args) throws InterruptedException {
 final Counter counter = new Counter();
 Thread t1 = new Thread(() -> {
 for (int i = 0; i < 50000; i++) {
 counter.increase();
 }
 });
 Thread t2 = new Thread(() -> {
 for (int i = 0; i < 50000; i++) {
 counter.increase();
 }
 });
 t1.start();
 t2.start();
 t1.join();
 t2.join();
 System.out.println(counter.count);
}

此时可以看到, 最终 count 的值仍然⽆法保证是 100000.

🌳wait 和 notify方法

由于线程之间是抢占式执⾏的, 因此线程之间执⾏的先后顺序难以预知.
但是实际开发中有时候我们希望合理的协调多个线程之间的执⾏先后顺序.
在这里插入图片描述
球场上的每个运动员都是独⽴的 “执⾏流” , 可以认为是⼀个 “线程”.
⽽完成⼀个具体的进攻得分动作, 则需要多个运动员相互配合, 按照⼀定的顺序执⾏⼀定的动作, 线程
1 先 “传球” , 线程2 才能 “扣篮”.

完成这个协调⼯作, 主要涉及到三个⽅法
• wait() / wait(long timeout): 让当前线程进⼊等待状态.
• notify() / notifyAll(): 唤醒在当前对象上等待的线程

注意: wait, notify, notifyAll 都是 Object 类的⽅法

🚩wait()

wait 做的事情:
• 使当前执⾏代码的线程进⾏等待. (把线程放到等待队列中)
• 释放当前的锁
• 满⾜⼀定条件时被唤醒, 重新尝试获取这个锁.

wait 要搭配 synchronized 来使⽤. 脱离 synchronized 使⽤ wait 会直接抛出异常.

wait 结束等待的条件:
• 其他线程调⽤该对象的 notify ⽅法.
• wait 等待时间超时 (wait ⽅法提供⼀个带有 timeout 参数的版本, 来指定等待时间).
• 其他线程调⽤该等待线程的 interrupted ⽅法, 导致 wait 抛出 InterruptedException 异常.

代码⽰例: 观察wait()⽅法使⽤

public static void main(String[] args) throws InterruptedException {
 Object object = new Object();
 synchronized (object) {
 System.out.println("等待中");
 object.wait();
 System.out.println("等待结束");
 }
}

这样在执⾏到object.wait()之后就⼀直等待下去,那么程序肯定不能⼀直这么等待下去了。这个时候就
需要使⽤到了另外⼀个⽅法唤醒的⽅法notify()。

🚩notify()

notify ⽅法是唤醒等待的线程.

• ⽅法notify()也要在同步⽅法或同步块中调⽤,该⽅法是⽤来通知那些可能等待该对象的对象锁的其
它线程,对其发出通知notify,并使它们重新获取该对象的对象锁。
• 如果有多个线程等待,则有线程调度器随机挑选出⼀个呈 wait 状态的线程。(并没有 “先来后到”)
• 在notify()⽅法后,当前线程不会⻢上释放该对象锁,要等到执⾏notify()⽅法的线程将程序执⾏
完,也就是退出同步代码块之后才会释放对象锁。

代码⽰例: 使⽤notify()⽅法唤醒线程

• 创建 WaitTask 类, 对应⼀个线程, run 内部循环调⽤ wait.
• 创建 NotifyTask 类, 对应另⼀个线程, 在 run 内部调⽤⼀次 notify
• 注意, WaitTask 和 NotifyTask 内部持有同⼀个 Object locker. WaitTask 和 NotifyTask 要想配合就
需要搭配同⼀个 Object.

static class WaitTask implements Runnable {
 private Object locker;
 public WaitTask(Object locker) {
 this.locker = locker;
 }
 @Override
 public void run() {
 synchronized (locker) {
 while (true) {
 try {
 System.out.println("wait 开始");
 locker.wait();
 System.out.println("wait 结束");
 } catch (InterruptedException e) {
 e.printStackTrace();
 }
 }
 }
 }
}
static class NotifyTask implements Runnable {
 private Object locker;
 public NotifyTask(Object locker) {
 this.locker = locker;
 }
 @Override
 public void run() {
 synchronized (locker) {
 System.out.println("notify 开始");
 locker.notify();
 System.out.println("notify 结束");
 }
 }
}
public static void main(String[] args) throws InterruptedException {
 Object locker = new Object();
 Thread t1 = new Thread(new WaitTask(locker));
 Thread t2 = new Thread(new NotifyTask(locker));
 t1.start();
 Thread.sleep(1000);
 t2.start();
}

🚩notifyAll()方法

notify⽅法只是唤醒某⼀个等待线程. 使⽤notifyAll⽅法可以⼀次唤醒所有的等待线程.
范例:使⽤notifyAll()⽅法唤醒所有等待线程, 在上⾯的代码基础上做出修改.

• 创建 3 个 WaitTask 实例. 1 个 NotifyTask 实例.

static class WaitTask implements Runnable {
 // 代码不变
}
static class NotifyTask implements Runnable {
 // 代码不变
}
public static void main(String[] args) throws InterruptedException {
 Object locker = new Object();
 Thread t1 = new Thread(new WaitTask(locker));
 Thread t3 = new Thread(new WaitTask(locker));
 Thread t4 = new Thread(new WaitTask(locker));
 Thread t2 = new Thread(new NotifyTask(locker));
 t1.start();
 t3.start();
 t4.start();
 Thread.sleep(1000);
 t2.start();
}

• 修改 NotifyTask 中的 run ⽅法, 把 notify 替换成 notifyAll

public void run() {
 synchronized (locker) {
 System.out.println("notify 开始");
 locker.notifyAll();
 System.out.println("notify 结束");
 }
}

此时可以看到, 调⽤ notifyAll 能同时唤醒 3 个wait 中的线程

注意: 虽然是同时唤醒 3 个线程, 但是这 3 个线程需要竞争锁. 所以并不是同时执⾏, ⽽仍然是有先有后的执⾏.

理解 notify 和 notifyAll
notify 只唤醒等待队列中的⼀个线程. 其他线程还是乖乖等着
在这里插入图片描述
notifyAll ⼀下全都唤醒, 需要这些线程重新竞争锁
在这里插入图片描述

⭕wait 和 sleep 的对比( 面试题)

其实理论上 wait 和 sleep 完全是没有可⽐性的,因为⼀个是⽤于线程之间的通信的,⼀个是让线程阻
塞⼀段时间,
唯⼀的相同点就是都可以让线程放弃执⾏⼀段时间.

当然为了⾯试的⽬的,我们还是总结下:

  1. wait 需要搭配 synchronized 使⽤. sleep 不需要.
  2. wait 是 Object 的⽅法 sleep 是 Thread 的静态⽅法

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

机器学习基础(四)非监督学习的进阶探索

导语&#xff1a;上一节我们详细探索监督学习的进阶应用&#xff0c;详情可见&#xff1a; 机器学习基础&#xff08;三&#xff09;监督学习的进阶探索-CSDN博客文章浏览阅读296次&#xff0c;点赞13次&#xff0c;收藏11次。监督学习作为机器学习的一个主要分支&#xff0c;…

RHEL9安装Python2.7

RHEL9作为2022年5月新推出的版本&#xff0c;较RHEL8有了很多地方的改进&#xff0c;而且自带很多包&#xff0c;功能非常强大&#xff0c;稳定性和流畅度也较先前版本有了很大的提升。RHEL9自带python3.9&#xff0c;但是过高版本的python不可避免地会导致一些旧版本包地不兼容…

【分布式事务 XA模式】MySQL XA模式详解

MYSQL中的XA事务 写在前面1. XA事务的基本原理2. MySQL XA事务操作 写在前面 MySQL 的 5.0.3 版本开始支持XA分布式事务&#xff0c;并且只有innoDB存储引擎支持XA事务。 1. XA事务的基本原理 XA事务本质上是一种基于两阶段提交的分布式事务&#xff0c;分布式事务可以理解成…

一些PCB整改优化经验总结

一个UP的PCB整改经验&#xff1a; 当正面全局铺铜之后出现很多小铜皮碎片的时候不如不铺铜或者单面铺铜RJ45网口的地和整体的地分开&#xff0c;两地之间通过电容相连&#xff08;整板地一定要相连&#xff09;TVS这种防浪涌高压的器件的地单独铺设&#xff0c;这样当高压来临…

《艾尔登法环 黄金树幽影》是什么?Mac电脑怎么玩《艾尔登法环》艾尔登法环下载

全体起立&#xff0c;《艾尔登法环 》最新DLC《黄金树幽影》将在6月21日发布&#xff0c;steam售价198元&#xff0c;现在就可以预订了。宫崎英高在接受FAMI通的采访时表示&#xff0c;新DLC的体量远超《黑暗之魂》和《血源诅咒》资料片。好家伙&#xff0c;别人是把DLC续作&am…

踩坑:SpringBoot连接Mysql的时区报错

解决方法&#xff1a;1.修改时区2.修改连接版本 目录 1.修改时区 2.切换版本 1.修改时区 查看mysql的默认时区 SELECT global.time_zone AS Global Time Zone, session.time_zone AS Session Time Zone; 查看mysqk的默认是时区返回两个结果 Global Time Zone:表示Mysql…

Jenkins中Publish Over SSH插件使用(1)

SSH插件 前言Publish Over SSH插件是jenkins里面必不可少的插件之一&#xff0c;主要的功能有两个把jenkins服务器上的文件&#xff0c;传输到远程nginx&#xff0c; 远程执行shell命令和脚本。 1. SSH插件下载与配置 1.1 下载Publish over SSH插件 系统管理—》管理插件 …

可控核聚变新里程碑!AI成功预测等离子体撕裂登Nature,清洁能源「圣杯」更近一步

可控核聚变&#xff0c;又有新突破了&#xff01; 长期以来&#xff0c;核聚变一直受着一个「幽灵」的困扰——等离子体不稳定性问题。 而最近&#xff0c;普林斯顿团队用AI提前300毫秒预测了核聚变等离子不稳定态&#xff0c;这个时间&#xff0c;就足够约束磁场调整应对等离…

美团外卖流程解析:便捷、高效、安全的美食配送

美团外卖作为中国最大的外卖平台之一&#xff0c;提供了丰富多样的美食选择&#xff0c;并通过高效的配送服务将美食送到用户手中。本文将深入探讨美团外卖的流程&#xff0c;从下单到送达&#xff0c;揭秘背后的便捷、高效、安全的运营体系。 1. 下单与支付 美团外卖的下单过…

[每周一更]-(第88期):Nginx 之 proxy_pass使用详解

proxy_pass 指令用于指定后端服务器的地址&#xff0c;可以采用以下不同的格式&#xff1a; 直接指定地址和端口&#xff1a; location / {proxy_pass http://backend_server:8080; }这将请求代理到 http://backend_server:8080。 使用变量&#xff1a; location / {set $ba…

bat脚本检测进程程序的方法

一、脚本检测进程 使用批处理脚本检测程序是否在运行&#xff0c;可以使用tasklist命令来列出当前运行的所有进程&#xff0c;并通过findstr命令来搜索特定的进程名。下面是一个简单的批处理脚本示例&#xff0c;它会检测指定的程序是否在运行&#xff0c;并给出相应的信息&…

PostgreSQL索引篇 | BTree

B-Tree索引 &#xff08;本文为《PostgreSQL数据库内核分析》一书的总结笔记&#xff0c;需要电子版的可私信我&#xff09; B树特点&#xff1a; 非叶子节点含一个或多个关键字值和子节点指针&#xff0c;不指向实际数据的存储位置所有关键字都是叶子节点&#xff0c;每个叶…

echarts多y轴样式重叠问题

1、主要属性设置 yAxis: [{//y轴1nameTextStyle: {align: "right",padding: 0}},{//y轴2nameTextStyle: {align: "left",padding: 0}},{//y轴3axisLabel: {margin: 50},nameTextStyle: {align: "left",padding: [0, 0, 0, 50]},axisPointer: {l…

华为HCIP Datacom H12-831 卷24

多选题 1、如图所示&#xff0c;某园区部署OSPF实现网络互通&#xff0c;其中Area1部署为NSSA区域。某工程师为了实现R1访问R4的环回口地址&#xff0c;在R4的OSPF进程中引入直连路由。以下关于该场景的描述,错误的有哪些项? A、在R4引入直连路由后&#xff0c;R1通过转换后的…

Sublime Text4配置C#运行环境

这里写自定义目录标题 前言部署.NET环境Sublime Text4配置C#编译环境1. 下载插件 运行测试 前言 今天把家里的9年前的远古神机搬了出来&#xff0c;重装了个win7的精简版&#xff0c;本打算装个VScode测试一下是否能写C#代码&#xff0c;结果是可以的&#xff0c;但&#xff0…

Python中format()方法的基本使用,第一种用法 <模板字符串>.format(<参数列表>)。

第一种用法&#xff1a; <模板字符串>.format(<参数列表>) 解析&#xff1a; 其中&#xff1a; <模板字符串>是包含占位符或者叫槽&#xff08;用花括号 {} 表示&#xff09;的字符串&#xff0c;用来指定最终格式化后的字符串的样式和结构。<参数列表…

Android 解决后台服务麦克风无法录音问题

Android 解决后台无法录音问题 问题分析问题来源解决方案1. 修改清单文件:`AndroidManifest.xml`2. 修改启动服务方式3. 服务启动时创建前台通知并且指定前台服务类型参考文档最后我还有一句话要说我用心为你考虑黄浦江的事情,你心里想的却只有苏州河的勾当 问题分析 安卓9.…

Android BitmapDrawable.bitmap与BitmapFactory.decodeResource获取不到原始图像素级真实宽高,Kotlin

Android BitmapDrawable.bitmap与BitmapFactory.decodeResource获取不到原始图像素级真实宽高&#xff0c;Kotlin 当一个图片放在ImageView里面后&#xff0c;用以下方式获取图的宽高&#xff1a; val bmp1 (this.drawable as BitmapDrawable).bitmapLog.d("fly", &…

Fiddler工具 — 21.Fiddler常用插件

Fiddler已有的功能已经够我们日常工作中使用了&#xff0c;为了更好的扩展Fiddler&#xff0c;Fiddler也是支持一些插件的安装&#xff0c;也支持用户自己开发插件并安装。 Fiddler插件下载地址&#xff1a;https://www.telerik.com/fiddler/add-ons 1、Traffic Differ Traf…

最优二叉搜索树 C#实现

最优二叉搜索树 C#实现 介绍一下 上一篇博文搞半天挺烧脑&#xff0c;没搞清楚继续… 主要是练习动态规划算法。最关键的一个是这个最优二叉搜索树能干啥。我认为如果数据稳定&#xff0c;统计出概率来&#xff0c;用最优二叉树保存&#xff0c;以后搜索应该是效率比较高的。…
最新文章