【JavaEE初阶】volatile 关键字、wait 和 notify

目录

一、volatile 关键字

1、volatile 能保证内存可见性

2、volatile 不保证原子性

二、wait 和 notify 

 1、wait()方法

 2、notify()方法

3、notifyAll()方法

4、wait 和 sleep 的对比


一、volatile 关键字

1、volatile 能保证内存可见性

我们前面的线程安全文章中,分析引起线程不安全的原因,其中就有一个原因是可见性,若一个线程对一个共享变量的修改,不能让其他线程看到,则会引起线程安全问题。因此,我们就引入了volatile 关键字,volatile 修饰的变量,能够保证 "内存可见性"。

(这里的“工作内存”不是真正的内存,就像CPU寄存器。)

代码在写入 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线程循环并不会结束,这是因为 t1 读的是自己工作内存中的内容,当 t2 对 flag 变量进行修改,此时 t1 感知不到 flag 的变化。

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

2、volatile 不保证原子性

volatile 和 synchronized 有着本质的区别,synchronized 能够保证原子性,volatile 保证的是内存可见 性。
这里可以用我们前面线程安全那一篇文章中的代码来证明,将 synchronized去掉,加上对count变量的 volatile 修饰。
public class ThreadDemo {private static volatile long count = 0;public static void main(String[] args) throws InterruptedException{Thread t1 = new Thread(()->{for (int i = 1;i <= 500000;i++) {count++;}});Thread t2 = new Thread(()->{for (long i = 0;i < 500000;i++) {count++;}});t1.start();t2.start();t1.join();t2.join();System.out.println("count= "+count);}
}

可见运行结果为:

 

此时,最终 count 的值仍然无法保证是 1000000。所以volatile 不保证原子性,volatile 保证的是内存可见性。

二、wait 和 notify 

由于线程之间是抢占式执行的,因此线程之间执行的先后顺序难以预知,但是实际开发中有时候我们希望合理的协调多个线程之间的执行先后顺序。
就比如打球,球场上的每个运动员都是独立的 "执行流" ,可以认为是⼀个 "线程"。而完成⼀个具体的进攻得分动作,则需要多个运动员相互配合,按照⼀定的顺序执行⼀定的动作,线程 1 先 "传球",线程 2 才能 "扣篮"。

完成这个协调工作,主要涉及到三个方法:

  • wait() / wait(long timeout):让当前线程进入等待状态
  • notify() / notifyAll():唤醒在当前对象上等待的线程

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

 1、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()。

 2、notify()方法

notify 方法是唤醒等待的线程的:
  • 方法notify()也要在同步方法或同步块中调用,该方法是用来通知那些可能等待该对象的对象锁的其它线程,对其发出通知notify,并使它们重新获取该对象的对象锁。
  • 如果有多个线程等待,则由线程调度器随机挑选出⼀个呈 wait 状态的线程。(并没有 "先来后到")
  • 在notify()方法后,当前线程不会马上释放该对象锁,要等到执行notify()方法的线程将程序执行完,也就是退出同步代码块之后才会释放对象锁
代码示例:使用notify()方法唤醒线程
  • 创建 WaitTask 类, 对应⼀个线程, run 内部循环调用wait.
  • 创建 NotifyTask 类, 对应另⼀个线程, 在 run 内部调用一次 notify
  • 注意, WaitTask 和 NotifyTask 内部持有同⼀个 Object locker,WaitTask 和 NotifyTask 要想配合就需要搭配同一个 Object
public class ThreadDemo {static class WaitTask implements Runnable {private Object locker;public WaitTask(Object locker) {this.locker = locker;}@Overridepublic 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;}@Overridepublic 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();}
}

3、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();}

此时可以看到,调用notify 只能唤醒⼀个线程

  • 修改 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 ⼀下全都唤醒,需要这些线程重新竞争锁

4、wait 和 sleep 的对比

其实理论上 wait 和 sleep 完全是没有可比性的,因为⼀个是用于线程之间的通信的,⼀个是让线程阻塞⼀段时间。
唯⼀的相同点就是都可以让线程放弃执行⼀段时间。
但我们还是要总结下:
  1. wait 需要搭配 synchronized 使用,sleep 不需要。
  2. wait 是 Object 的方法,sleep 是 Thread 的静态方法。

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

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Leetcode394. 字符串解码

Every day a Leetcode 题目来源&#xff1a;394. 字符串解码 解法1&#xff1a;栈 本题中可能出现括号嵌套的情况&#xff0c;比如 2[a2[bc]]&#xff0c;这种情况下我们可以先转化成 2[abcbc]&#xff0c;在转化成 abcbcabcbc。我们可以把字母、数字和括号看成是独立的 TO…

vs配置64位汇编

vs开发64位程序无法使用内联汇编&#xff0c;需要将汇编放到一个单独文件中编译链接。 步骤如下&#xff1a; 生成汇编代码。以asm.asm为例&#xff0c;以下是模板&#xff1a; ;64位汇编程序模板 (Template) ;声明一个ExitProcess函数 ExitProcess PROTO.data;在这里声明变量…

关于pyqt5与moviepy到打包的坑点

1,pyqt5 关于pyqt5 designer.exe 的使用主要就是了解pyqt5右侧菜单栏的功能使用 打包后的文件&#xff0c;需要继承改类&#xff0c;进行图形指令交互 关于pyqt5&#xff0c;要了解信号&#xff0c;和槽点的相互关系。 我在pyqt5中使用moviepy的时候&#xff0c;需要用到异步…

行测空间展开图类型题目通法

国考《行测》空间展开图类型题目通法 摘要 本文介绍一种判断哪一种立体图形可以由空间展开图组成的通用方法。 方法 1.将给定的展开图重构为容易识别的上边一个方块&#xff0c;下边一个方块&#xff0c;中间一个方块条的形式。 姑且称之为十字形吧。 之所以重构成这种形…

WordPress批量上传文章和自动发布文章的方法

专业介绍&#xff1a;WordPress批量上传文章技术解析 在现代数字时代&#xff0c;内容创作是网络存在的驱动力之一。对于博客作者、新闻编辑和内容管理员而言&#xff0c;高效地批量上传文章至WordPress平台是提高工作效率的一个关键方面。WordPress作为最受欢迎的内容管理系统…

能耗远程在线监测系统在工业节能提高效率

摘要&#xff1a;为保证企业实现节能减排目标&#xff0c;设计和使用远程在线监测系统势在必行。远程在线监测系统是基于传感器与网络技术的优势&#xff0c;在企业区域各个位置针对性安装传感器&#xff0c;对实时数据进行采集、编码传输到远程管理系统。远程管理系统对采集的…

Java核心知识点整理大全24-笔记

22. 数据结构 22.1.1. 栈&#xff08;stack&#xff09; 栈&#xff08;stack&#xff09;是限制插入和删除只能在一个位置上进行的表&#xff0c;该位置是表的末端&#xff0c;叫做栈顶 &#xff08;top&#xff09;。它是后进先出&#xff08;LIFO&#xff09;的。对栈的基…

Docker Swarm总结+CI/CD Devops、gitlab、sonarqube以及harbor的安装集成配置(3/5)

博主介绍&#xff1a;Java领域优质创作者,博客之星城市赛道TOP20、专注于前端流行技术框架、Java后端技术领域、项目实战运维以及GIS地理信息领域。 &#x1f345;文末获取源码下载地址&#x1f345; &#x1f447;&#x1f3fb; 精彩专栏推荐订阅&#x1f447;&#x1f3fb;…

Windows系统搭建Appium 2 和 Appium Inspector 环境

前言 自 2022 年 1 月 1 日起&#xff0c;Appium 核心团队不再维护 Appium 1.x。官方支持的平台驱动程序的所有最新版本均不兼容 Appium 1.x&#xff0c;需要 Appium 2 才能运行。 Appium 2是一个自动化移动应用程序的开源工具&#xff0c;它带来了以下重要改进&#xff1a;  …

揭秘近期CSGO搬砖市场小幅回暖的真正原因

最近市场小幅度回暖&#xff0c;第一个原因则是到处都在说buff要开租赁了&#xff0c;市场要开始爆燃了。阿阳听到这些消息实在是绷不住了&#xff0c;出来给大家讲一下自己的看法&#xff0c;大家理性思考一下。 Buff出不出租赁跟市场燃不燃有一点关系吗&#xff1f;隔壁悠悠…

C++基础 -20- 基类覆盖父类

引用的方式覆盖 #include "iostream" using namespace std; class base { public:base() {}base(int a, int b) : a(a), b(b){}int a;int b; }; class step1 : public ::base { public:step1() {} };int main() {step1 rlxy;rlxy.a 100;rlxy.b 200;cout <<…

强化学习-DQN

网上看来很多&#xff0c;但是还是觉得这篇文章将得最好&#xff1a; 可视化强化学习解释 - Deep Q Networks&#xff0c;循序渐进 |Ketan Doshi 博客 (ketanhdoshi.github.io)