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

news2024/12/22 13:58:08

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.coloradmin.cn/o/1467746.html

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

相关文章

C#中的关键字params的用法

C#中有一个关键字params&#xff0c;它相对于一些主要关键字来说&#xff0c;还算是较为低频的&#xff0c;但也会用到。我们可以了解和学习下。 一、定义及约束 params关键字的作用在于可以让方法参数的数目可变。 params的参数类型必须是一维数组。 一旦在方法加入了para…

【Java】输入输出流(实验八)

目录 一、实验目的 二、实验内容 三、实验小结 一、实验目的 1、掌握java I/O的基本原理。 2、掌握标准输入输出流和Scanner类的基本使用方法。 3、掌握FileInputStream、FileOutStream、FileReader、FileWriter、BufferedReader 、BufferedWriter类的常用方法。 二、实验…

电商行业网络安全——守护数字世界的交易安全与信任

随着互联网的迅猛发展&#xff0c;电子商务行业在全球范围内呈现出爆炸性增长的态势。然而&#xff0c;伴随着网络交易的便捷性&#xff0c;网络安全问题也愈发凸显&#xff0c;成为电商行业必须严肃面对的挑战。在这个数字化、信息化的时代&#xff0c;电商行业的网络安全不仅…

v-rep插件

v-rep官网插件汉化教程 官网教程 插件是什么 插件本质上就是遵循一定规范的API编写出来的程序&#xff0c;在v-rep中最终需要编译为动态库。 linux下是libsimXXXX.so&#xff1b; 其中XXXX是插件的名称。 请至少使用4个字符&#xff0c;并且不要使用下划线&#xff0c;因为…

力扣精选100道——外观数列(模拟专题)

外观数列算法题链接 &#x1f6a9;了解题意 该题的下面充分的给你说明了这个题目的意思。 3 3 2 2 2 5 1 我们根据我们正常读的顺序读 俩个3 三个2 一个5 一个1 连起来就是 2 3 3 2 1 5 1 这就是最终输出的字符串。 题目开头说了&#xff0c;我们最初是 1开始读…

c语言经典测试题4

1.题1 #include <stdio.h>//没有break的话&#xff0c;输入什么都会往下一直执行下去&#xff0c;而且default在最后就会全都执行 int main() {char c;int v0 0, v1 0, v2 0;do{switch (c getchar())// 输入ADescriptor{casea:caseA:casee:caseE:casei:caseI:caseo:…

HTTP概要

文章目录 什么是HTTP?URL的结构请求报文结构请求方法GETHEADPOSTPUTDELETETRACEOPTIONSCONNECTPATCH解释 请求头字段 响应报文结构响应状态响应头字段 HTTP会话3次握手无状态协议 什么是HTTP? HTTP&#xff0c;即Hypertext Transfer Protocol(超文本传输协议) 它是一个”请…

有没有哪些适合程序员的副业?

夸克网盘这个软件出来好久了&#xff0c;官方前不久才开通了推广渠道&#xff0c;这就给了我们以此赚钱的机会。具体时间应该是在2022年12月份。 所谓夸克网盘拉新&#xff0c;就是夸克网盘为了抢占市场&#xff0c;与其他网盘竞争对手&#xff08;百度网盘、迅雷网盘等&#…

CVE-2023-44313 Apache ServiceComb Service-Center SSRF 漏洞研究

本次项目基于go语言&#xff08;本人不精通&#xff09;&#xff0c;虽不是java web框架了 &#xff0c;但搭建web服务的框架一些思想理念却是通用的&#xff0c;我们由此可以得到一些蛛丝马迹....... 目录 漏洞简介 漏洞分析 漏洞复现 漏洞简介 Apache ServiceComb Servi…

Python环境下一种简单的基于域自适应迁移学习的轴承故障诊断方法

域自适应是指在源域和目标域之间进行相同的迁移学习任务&#xff0c;由于两个领域的数据分布不一致&#xff0c;源域中存在大量的带标签的样本&#xff0c;目标域则没有&#xff08;或极少&#xff09;带标签的样本。通过这种方式可以将在源域样本中学到的知识迁移到目标域上&a…

com.alibaba.nacos.api.exception.NacosException: Request nacos server failed

问题描述 安装nacos2.0以上版本&#xff0c;启动报错:com.alibaba.nacos.api.exception.NacosException: Request nacos server failed com.alibaba.nacos.api.exception.NacosException: Request nacos server failed: at com.alibaba.nacos.client.naming.remote.gprc.Nami…

[C#]winform基于opencvsharp结合CSRNet算法实现低光图像增强黑暗图片变亮变清晰

【算法介绍】 "Conditional Sequential Modulation for Efficient Global Image Retouching" 是一种图像修饰方法&#xff0c;主要用于对图像进行全局的高效调整。该方法基于深度学习技术&#xff0c;通过引入条件向量来实现对图像特征的调制&#xff0c;以达到改善…

Polyspace静态检测步骤

Polyspace 是一个代码静态分析和验证的工具&#xff0c;隶属于MATLAB&#xff0c;用于检测代码中的错误和缺陷&#xff0c;包括内存泄漏、数组越界、空指针引用等。帮助开发团队提高代码质量&#xff0c;减少软件开发过程中的错误和风险。 1、打开MATLAB R2018b 2、找到Polys…

“从根到叶:深入理解排序数据结构“

一.排序的概念及引用 1.1排序的概念 排序是指将一组数据按照一定的规则重新排列的过程。排序的目的是为了使数据具有有序性&#xff0c;便于查找、插入、删除等操作&#xff0c;提高数据的组织和管理效率。 稳定性是指如果序列中存在相等元素&#xff0c;在排序完成后&#…

【简单明了,一文讲解】数据结构与算法基础入门篇--算法之排序篇

图1. 小林Coding整理图 排序算法是计算机科学中常见的一类算法&#xff0c;用于将一组数据按照一定规则进行排序。 常见的排序算法包括以下几种&#xff1a; 冒泡排序&#xff08;Bubble Sort&#xff09;&#xff1a;通过相邻元素的比较和交换来实现排序&#xff0c;每一轮将最…

GEE必须会教程—邂逅线代中的矩阵(Array类型)

矩阵&#xff0c;一个令人头疼的名字&#xff0c;学过线性代数的友友们想必对矩阵的运算规则烂熟于心&#xff0c;与它延申出来的向量知识曾经让我们深陷其中。矩阵在高级的数据存储中占据着重要的地位。定义字典类型的过程&#xff0c;其实就是寻找key和value关系的过程&#…

OpenCV 4基础篇| OpenCV图像基本操作

目录 1. 图像读取1.1 cv2.imread() 不能读取中文路径和中文名称1.2 cv2.imdecode() 可以读取中文路径和中文名称 2. 图像的显示2.1 openCV显示图像 cv2.imshow()2.2 matplotlib显示图像 plt.imshow() 3. 图像的保存 cv2.imwrite()4. 图像的复制4.1 img.copy()4.2 np.copy()4.3 …

贪婪算法入门指南

想象一下&#xff0c;你在玩一款捡金币的游戏。在这个游戏里&#xff0c;地图中散布着各种大小不一的金币&#xff0c;而你的目标就是尽可能快地收集到最多的金币。你可能会采取一个直观的策略&#xff1a;每次都去捡最近的、看起来最大的金币。这种在每一步都采取局部最优解的…

ONLYOFFICE 桌面编辑器现已更新至v8.0啦

希望你开心&#xff0c;希望你健康&#xff0c;希望你幸福&#xff0c;希望你点赞&#xff01; 最后的最后&#xff0c;关注喵&#xff0c;关注喵&#xff0c;关注喵&#xff0c;佬佬会看到更多有趣的博客哦&#xff01;&#xff01;&#xff01; 喵喵喵&#xff0c;你对我真的…

ETL:数据转换与集成的关键过程

ETL&#xff1a;数据转换与集成的关键过程 在现代数据驱动的世界中&#xff0c;有效地管理和处理数据对于企业的成功至关重要。ETL&#xff08;提取、转换、加载&#xff09;是一种关键的数据处理过程&#xff0c;有助于将数据从源系统提取、清洗、转换并加载到目标系统中&…