【多线程】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;}@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();
}

🚩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.mzph.cn/news/700098.shtml

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

相关文章

yolo导出的onnx怎么使用onnxruntime进行图片分割

1.上几篇文件写了&#xff0c;怎么自定义自己的yolo数据&#xff0c;怎么训练自己自定义的数据&#xff0c;怎么用onnxruntime调用yolo导出的onnx模型&#xff0c;这文章进一步写用onnxruntime调用yolo导出的onnx模型进行图片分割。 视频讲解地址:https://www.bilibili.com/vi…

Java学习——常用API Runtime

Java的Runtime类是用来与Java运行时环境进行交互的。它提供了执行系统命令、获取内存信息等功能。Runtime类是一个单例类&#xff0c;即在Java应用程序中只能获得一个Runtime实例&#xff0c;用于代表Java应用程序的运行时环境。 获取Runtime实例 由于Runtime类的构造函数是私…

【GTK+-编程指南*】

GTK-编程指南 ■ GTK 简介 (GIMP Toolkit)■ GTK和GTK■ GTK 常用的库■ GTK 环境搭建■ GTK 语法■ GTK ■ GTK 简介 (GIMP Toolkit) GIMP全称&#xff1a;GNU 图像处理程序(Gnu Image Manipulation Program) GTK 是一套源码以LGPL许可协议分发、跨平台的图形工具包。 GTK也是…

基础光学系列:(一)光学在机器视觉中的角色:原理、应用与学习途径

光学是一门研究光的产生、传播以及与物质相互作用的科学&#xff0c;对于机器视觉技术的发展至关重要。机器视觉利用计算机和相机系统模拟人类视觉&#xff0c;解释和理解图像&#xff0c;广泛应用于制造业、医疗、安全监控等领域。本文旨在探讨光的传播原理及其在机器视觉中的…

处理器后面的字母什么意思?

我们在挑选电脑时&#xff0c;会见到各种各样的CPU型号&#xff0c;很多人可能不太了解处理器后面的各字母究竟代表着什么&#xff0c;今天我和大家分享一下常见的几种处理器后缀。 一&#xff0c;U 处理器后面跟字母U代表着移动平台低电压处理器&#xff0c;移动平台也就是笔…

低于API等级30的应用将无法在上述应用商店

minSdkVersion minSdkVersion用于指定应用兼容的最低Android版本&#xff08;API等级&#xff09;。 如果APP某些功能无法支持低版本Android系统的设备&#xff0c;可以配置minSdkVersion确保APP只能安装到指定Android版本以上的设备。HBuilder|HBuilderX中可在manifest.json中…

Essential C++ 基于对象的编程风格

Essential C 前言 前言 通过Essential C笔记的形式对C相关重点知识进行汇总&#xff0c;读者通读此系列文章就可以轻松的把该语言基础捡起来。

Linux的目录操作

Linux目录操作 获取当前工作目录 getcwd() 声明&#xff1a;char *getcwd(char *buf, size_t size);使用方法&#xff1a; getcwd用于获取当前工作目录&#xff0c;并将其保存在buf指向的缓冲区中。返回值&#xff1a; 返回一个指向buf的指针&#xff0c;若失败则返回NULL。注…

Linux快速修改ip地址

Linux修改IP配置 一 、查找ip配置文件 ifcfg-ens33二、编辑 vi ifcfg-ens33文件三、重启网络或者重启系统 一 、查找ip配置文件 ifcfg-ens33 cd /etc/sysconfig/network-scripts/ls //查看network-scripts文件夹下面的文件二、编辑 vi ifcfg-ens33文件 vi ifcfg-ens33注意&…

机器学习 深度学习资料 资源machine learning

Kaggle入门&#xff0c;看这一篇就够了 - 知乎 (zhihu.com)https://zhuanlan.zhihu.com/p/25686876 day1-1.什么是机器学习_哔哩哔哩_bilibiliday1-1.什么是机器学习是10天学会机器学习从入门到深度学习的第1集视频&#xff0c;该合集共计62集&#xff0c;视频收藏或关注UP主&a…

【MySQL】如何理解MySQL的存储引擎

一、常见存储引擎 我们可以知道表的类型是由存储引擎决定的&#xff0c;如图所示&#xff0c;我们可以看到MySQL中支持八种储存引擎&#xff0c;那么我们这里就介绍常用的三种存储引擎&#xff0c;分别是Innodb、MyISAM、Memory。 二、Innodb、MyISAM、Memory的特点 1、Inno…

day10-项目集成sharding-jdbc-今日指数

项目集成sharding-jdbc 目标 1.掌握shardingjdbc标准分片模式&#xff1b; 2.理解项目分库分表的设计思路&#xff1b; 3.理解分库分表代码实现流程&#xff1b; 第一章 项目分库分表实现 1、股票数据预期增长分析 ​ 由于今日指数是偏向海量数据分析的产品&#xff0c;而…

Go语言基础总结

一、Go语言结构 包声明 引入包 函数 变量 语句&表达式 注释 下面简单给出hello.go文件。 package src /*定义包名*/import "fmt" /*引入包*/func hello() { /*函数*/fmt.Println("Hello,World!") /*语句&表达式*/fmt.Println("菜鸟教…

2024年华为OD机试真题-抢7游戏-Python-OD统一考试(C卷)

题目描述: A、B两个人玩抢7游戏,游戏规则为A先报一个起始数字X(10<起始数字<10000),B报下一个数字Y(X-Y<3),A再报一个数字Z(Y-Z<3),以此类推,直到其中一个抢到7,抢到7即为胜者;在B赢得比赛的情况下,一共有多少种组合? 输入描述: 起始数字M,如100;10<…

Linux系统下防火墙端口操作

防火墙端口的认识 防火墙是一种网络安全设备&#xff0c;用于监控和控制网络进出流量&#xff0c;其中开放端口是防火墙功能中的一个重要部分&#xff0c;可以确保应用程序和服务正常通信。端口是不同应用程序或服务之间通信的入口或出口&#xff0c;每个端口都有一个数字标识…

go - 学习笔记 - 1

go - 学习笔记 -1 hello world变量值拷贝匿名变量语句分隔符整型&浮点型&布尔型字符串类型strings包的字符串函数 hello world package mainimport "fmt"func main() {fmt.Println("hello world") }变量 package mainimport "fmt"func…

深度学习环境配置常见指令

首先打开anaconda prompt&#xff0c;激活对应虚拟环境。 导入torch并获取对应版本 import torch torch.__version__导入torchvision并获取对应版本 import torchvision torchvision.__version__ 检查cuda是否可用 torch.cuda.is_available() 获取CUDA设备数 torch.cuda.…

OpenBMC的c++代码中的变量初始化问题(一)

1 开发平台 Win11、VS2022、Fedora39。 2 作业目的 通过VS2022跨平台Linux构建openbmc/intel-ipmi-oem的x64可执行模块。 3 问题描述 该模块启动后&#xff0c;在执行subprojects\phosphor-host-ipmid\user_channel\channel_mgmt.cpp 中的函数convertToMediumTypeIndex出现…

leetcode-回溯法-矩阵中的路径

https://www.nowcoder.com/practice/c61c6999eecb4b8f88a98f66b273a3cc?tpId13&tqId11218&tPage4&rp4&ru/ta/coding-interviews&qru/ta/coding-interviews/question-ranking 请设计一个函数&#xff0c;用来判断在一个矩阵中是否存在一条包含某字符串所有…

基于SpringBoot的气象数据监测分析大屏

项目描述 临近学期结束&#xff0c;还是毕业设计&#xff0c;你还在做java程序网络编程&#xff0c;期末作业&#xff0c;老师的作业要求觉得大了吗?不知道毕业设计该怎么办?网页功能的数量是否太多?没有合适的类型或系统?等等。这里根据疫情当下&#xff0c;你想解决的问…