Java多线程详解⑦(全程干货!!!)内存可见性 || volatile || JMM || wait notify notifyAll

 这里是Themberfue

· 在上一节中,我们讨论了死锁的概念,产生的场景 ,产生的必要条件......


内存可见性 

· 我们先来看一段百度百科关于 "内存可见性"  的解释

· "内存可见性" 就是造成线程安全问题的原因之一

· 如果是单纯地看这段文字,可能会比较难读,我们通过一段代码来深入理解这个问题

public class Demo21 {public static int flag = 0;public static void main(String[] args) throws InterruptedException {Thread t1 = new Thread(() -> {while (flag == 0) {}System.out.println("t1线程结束");});Thread t2 = new Thread(() -> {try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}flag = 1;System.out.println("t2线程结束");});t1.start();t2.start();t1.join();t2.join();}
}

· 这段不难读懂,t1 线程判断变量 flag 的改变情况,若为 0,则 t1 线程循环执行,若被修改后,则 t1 线程会立刻终止执行

· t2 线程执行修改变量 flag 的任务,但并非立刻执行修改的逻辑,而是1ms 后

· 这样的代码理论上并无问题,在 1s 后,两个线程都会结束

· 但实际运行后,你会发现虽然 t2 线程结束了,但是 t1 线程却迟迟没有结束,查看变量 flag 的值,也确实更改为了 1,但为什么 t1 线程没有结束呢?

· 众所周知,程序员之间的水平都是参差不平的

· 优化代码这件事本来就是很微妙的,程序都能跑,那我就懒得优化了,这绝对是大多数程序员的写照(但是,代码优化也是非常重要的事情,还是要注意的)

· 研究 JDK 的大佬们,就希望通过 编译器 或者 JVM 来给我们写出的代码自动优化,在对逻辑不变的前提下,对代码的结构进行一些小幅度的调整从而使代码更加高效的运行

· 这样的事看起来当然好,但是难免地会引发弊端,尤其是在编写多线程代码时,编译器 或者 JVM 的优化可能就会判断失误,导致逻辑理论上是正确的,但结果却大不尽人如意

·  "内存可见性" 就是由于 JVM 自动优化代码所造成的

· 我们先来了解汇编语言中的一个指令:"cmp" 本质是比较(compare)指令,通常用于控制程序流程(例如条件判断和分支),cmp 本质是一个隐式减法操作

while (flag == 0) {}

· 上述伪代码就是一个条件判断,在执行 "cmp" 指令之前,程序还得先执行 load 指令(在x86架构中是用 mov 指令执行类似的操作),该指令将内存中的数据加载到寄存器里

· 随后在执行 cmp 指令,所以 cmp 指令本质是纯 寄存器 操作的,而 load 指令涉及到内存的操作

· 回到最开始的代码,在 t1 线程运行后,while循环里的条件判断疯狂反复执行,判断变量 flag 是否变化了,所以在短时间内,其会执行几百上千次

· 在这个过程中,JVM 发现我始终从 内存 中读取到的值都是不变的,所以 JVM 就不从内存上读了,直接从寄存器上读了

· 因为相比于单纯地寄存器操作,内存操作的时间开销是非常大的,所以 JVM 就会帮助我们  "优化" 代码,后续 t2 线程修改内存中的变量 flag 值后,t1 线程也不会察觉到,这边就是产生 bug 的原因

 · 如果稍微调整代码,让这个 while 循环里的逻辑加上 sleep(1)

public class Demo21 {public static int flag = 0;public static void main(String[] args) throws InterruptedException {Thread t1 = new Thread(() -> {while (flag == 0) {try {Thread.sleep(10);} catch (InterruptedException e) {throw new RuntimeException(e);}}System.out.println("t1线程结束");});Thread t2 = new Thread(() -> {try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}flag = 1;System.out.println("t2线程结束");});t1.start();t2.start();t1.join();t2.join();}
}

· 运行代码后,惊人地发现,这个问题解决啦!程序正常结束了!

· 这是为什么呢?

        计算机的指令级操作一般都是纳秒级别的,本来程序是飞快执行 while 循环,但突然插入了一个 1ms 的等待,此时,JVM 对优化变量 flag 就感觉无足轻重了,自然程序就正常结束了

· 那这是否意味着这种问题以后都这样解决吗?

        不是的,使用 sleep 会大大影响程序的运行效率,所以非常不推荐这样做


volatile 

· 为了解决上述问题,Java5 引入了 volatile 关键字

· 通过这个关键字修饰的变量,就不会被编译器优化成单纯从读寄存器读取了

public class Demo22 {// 通过 volatile 关键字来修饰变量,那么该变量就不会被JVM优化操作了// 该关键字只能修饰变量public volatile static int flag = 0;public static void main(String[] args) throws InterruptedException {Thread t1 = new Thread(() -> {while (flag == 0) {}System.out.println("t1线程结束");});Thread t2 = new Thread(() -> {try {Thread.sleep(1);} catch (InterruptedException e) {throw new RuntimeException(e);}flag = 1;System.out.println("t2线程结束");});t1.start();t2.start();t1.join();t2.join();}
}

· 这样问题才真正得以解决


JMM

· JMM(Java Memory Model):Java内存模型

· 其是一种抽象的概念,为了更好的管理 Java 对内存的管理而定义出的一套定义和规则。虽然说编程语言可以直接地复用操作系统层面的内存模型,但是,不同的操作系统的内存模型一般都是不同的,这不符合 Java 跨平台的特性

· 于是,JMM就出现了,不仅抽象了线程与主内存之间的关系,还规定了 Java 源代码到 CPU 可执行指令的这个转化过程要遵守哪些和并发相关的原则和规范,主要目的是简化多线程编程,提高程序的可移植性

· 最主要的还是:每个线程,有一个自己的 "工作内存" ,同时这些线程又共享一个 "主内存"

· 当一个线程循环执行上述的读取变量的操作时,就会先从 "主内存" 中的数据读取到 该线程的 "工作内存"当中

· 后续另一个线程修改时,也是想修改自己 "工作内存" 的数据,再拷贝到 "主内存" 中

· JMM(Java 内存模型)主要定义了对于一个共享变量,当另一个线程对这个共享变量执行写操作后,这个线程对这个共享变量的可见性

· 这里我就不深入讲解了,想要了解更多,可以参考下面的文章

JMM 详解


wait | notify | notifyAll 

· 在之前的多线程编程中,两个或多个线程之间的并发操作都是随机的,我们虽然可以通过  sleep方法,join方法,加锁操作对线程执行的顺序或多或少的进行一些人为地控制

· 但这些方法通常考虑的都不会那么全面,尽管人为地进行了控制,但是其等待还是不确定的

· wait(等待) 和 notify(通知) 就是协调线程之间的执行逻辑的顺序的

· 其可以让后执行的逻辑等待先执行的逻辑,虽然无法直接干预调度器的调度顺序,但是可以让后执行的线程的某一段逻辑先 "等待",先执行的线程的某一逻辑执行完毕后,再 "通知" 先前等待的线程继续执行后续的逻辑

· wait 和 notify 都是 Object 提供的方法,也就是说,任何对象都可以调用这两个方法。就如同任何对象都可以当作锁对象一样

object.wait();

· 如果你直接使用上述代码,程序便会抛出一个 IllegalMonitorStateException 异常。为什么会抛出这个异常?

· 首先,wait方法执行后会先释放这个线程加的锁,再让其阻塞等待。在释放锁的前提下应该处于加锁状态,才能释放

· 你都没有加锁,谈何释放?为什么要先释放锁?很简单的一个问题:占着茅坑不拉屎~~~

· 虽然 wait 后先释放锁,但是后续被其他线程 notify 唤醒后,这个线程后续的执行逻辑又会重新加上锁,保证线程安全

· 虽然说 notify 不需要先释放锁在调用 notify,但是 Java 规定其必须先加锁后才可以使用 notify。操作系统中,也有相关的 api 接口,但是其 notify 不需要和锁搭配使用

public class Demo23 {public static void main(String[] args) {Object locker = new Object();Thread t1 = new Thread(() -> {try {synchronized (locker) {System.out.println("wait之前");locker.wait();System.out.println("wait之后");}} catch (InterruptedException e) {throw new RuntimeException(e);}});t1.start();Scanner sc = new Scanner(System.in);System.out.println("输入任意内容,唤醒t1");sc.next();// 必须保证 notify 在 wait 之前,不然唤醒个寂寞synchronized (locker) {locker.notify();}}
}

 

· 这四处都必须使用同一个锁对象,否则其不会生效

· 就好比这个锁对象是这两个线程之间沟通的 "桥梁"

· 必须保证 notify 在 wait 之前,不然唤醒个寂寞

· 如果有多个线程被同一个锁调用 wait 等待时,使用 notifyAll 可以唤醒所有这些等待的线程,所有被选择的线程从等待状态转移到阻塞状态,直到当前线程释放锁

public class Demo24 {public static void main(String[] args) throws InterruptedException {Object locker = new Object();Thread t1 = new Thread(() -> {try {synchronized (locker) {System.out.println("t1 等待");locker.wait();System.out.println("t1 被唤醒");}} catch (InterruptedException e) {throw new RuntimeException(e);}});Thread t2 = new Thread(() -> {try {synchronized (locker) {System.out.println("t2 等待");locker.wait();System.out.println("t2 被唤醒");}} catch (InterruptedException e) {throw new RuntimeException(e);}});t1.start();t2.start();Thread.sleep(100);// 针对多个线程都在同一个对象上 wait,notify 每次只会随机唤醒一个线程// notifyAll 可以唤醒全部线程synchronized (locker) {locker.notifyAll();}}
}

PS:如果有多个线程针对一个锁对象进行 wait,那么另一个线程对该锁对象进行 notify 只会唤醒随机一个线程


· wait 和 join 类似,也提供可以提供指定时间的等待:

public class Demo25 {public static void main(String[] args) {Object locker = new Object();Thread t1 = new Thread(() -> {try {synchronized (locker) {System.out.println("t1等待");locker.wait(5000);System.out.println("超过5秒,t1被唤醒");}} catch (InterruptedException e) {throw new RuntimeException();}});t1.start();Scanner sc = new Scanner(System.in);System.out.println("输入任意内容,唤醒 t1");sc.next();synchronized (locker) {locker.notify();}}
}

· 加上时间的 wait 和 sleep 看似没有区别,都是指定时间的等待

· 实则还是有点区别的:

        1. wait 必须搭配锁使用,必须先加锁才可以使用,而 sleep 不需要

        2. 如果都是在 synchronized 代码块触发,wait 则需要先释放锁,再阻塞等待,然而 sleep 不会释放锁,而是依然占用着锁等待,这显然是不合理的


多线程小案例

题目:

        有三个线程,分别只能打印 A,B,C,要求按顺序打印出 ABC,打印 10 次

public class Demo26 {public static void main(String[] args) throws InterruptedException {Object locker1 = new Object();Object locker2 = new Object();Object locker3 = new Object();Thread t1 = new Thread(() -> {try {for (int i = 0; i < 10; i++) {synchronized (locker1) {locker1.wait();}System.out.print('A');synchronized (locker2) {locker2.notify();}}} catch (InterruptedException e) {throw new RuntimeException();}});Thread t2 = new Thread(() -> {try {for (int i = 0; i < 10; i++) {synchronized (locker2) {locker2.wait();}System.out.print('B');synchronized (locker3) {locker3.notify();}}} catch (InterruptedException e) {throw new RuntimeException();}});Thread t3 = new Thread(() -> {try {for (int i = 0; i < 10; i++) {synchronized (locker3) {locker3.wait();}System.out.println('C');synchronized (locker1) {locker1.notify();}}} catch (InterruptedException e) {throw new RuntimeException();}});t1.start();t2.start();t3.start();Thread.sleep(100);synchronized (locker1) {locker1.notify();}}
}

· 巧妙利用 wait notify 可以简单快速解决这个问题 


· 毕竟不知后事如何,且听下回分解~~

 

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

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

相关文章

安装双系统(linux操作系统(debian)安装)

参考博客&#xff1a;戴尔服务器安装Debian11过程_戴尔t130安装debian-CSDN博客 一.腾出一个50G以上的空间&#xff0c;准备装操作系统 1.底部搜索计算机管理&#xff0c;选择磁盘管理 本人已预留400GB磁盘空间安装ubuntu系统&#xff0c;若没有预留空间&#xff0c;则可以选…

心系天下三星W25:记录盛世影像 见证华彩时光

悠然岁月中&#xff0c;被定格的瞬间总是历久弥新。心系天下三星W25以传世经典之姿跃然于掌中&#xff0c;将精致外形与精湛工艺合二为一&#xff0c;彰显出持有者的高雅气质。同时&#xff0c;强悍的影像系统则使之成为光影艺术的记录者&#xff0c;无论是捕捉人生风华&#x…

修改msyql用户密码及更新mysql密码策略

查看mysql中初始的密码策略 SHOW VARIABLES LIKE validate_password% 2. 修改默认密码策略 -- 0 或者 LOW 只验证长度-- 1 或者 MEDIUM 验证长度、数字、大小写、特殊字符-- 2 或者 STRONG 验证长度、数字、大小写、特殊字符、字典文件set global validate_password.policy0;…

f.lux电脑屏幕护眼神器,告别熬夜伤眼

前言 之前分享了一款护眼宝&#xff0c;也是可以调节屏幕色温&#xff0c;但是都是需要手动调节 今天分享一款通过智能调节屏幕亮度&#xff0c;减少长时间使用电脑对眼睛的伤害。它可以根据环境光线和用户的使用习惯&#xff0c;自动调整屏幕亮度&#xff0c;确保用户在不同时…

小家电器产品三维动画渲染怎么做快一些?

在快节奏的市场竞争中&#xff0c;快速制作小家电器产品的三维动画渲染显得尤为重要。本文将为您揭示如何高效完成这一过程&#xff0c;让您的产品以最直观的方式吸引消费者的目光。 一、电器产品动画渲染需要软件 原文出自 电器产品三维动画渲染怎么做-电器产品3D动画渲染需要…

Cesium基础-(Entity)-(model )

里边包含Vue、React框架代码详细步骤、以及代码详细解释 公众号:GISer世界 三维模型地址 :https://download.csdn.net/download/weixin_44857463/89986869 效果: 以下是Cesium中Model的属性和方法: 属性 属性名称类型默认值描述urlstring | Resource.gltf或.glb文件的URLb…

RAG综述:《A Comprehensive Survey of Retrieval-Augmented Generation (RAG)》

来源于《A Comprehensive Survey of Retrieval-Augmented Generation (RAG): Evolution, Current Landscape and Future Directions》 一、RAG所解决的问题 如何有效地从外部知识源检索相关信息&#xff0c;如何将这些信息无缝地融入到生成文本中&#xff0c;以及如何在保证生…

带你读懂什么是AI Agent智能体

一、智能体的定义与特性 定义&#xff1a;智能体是一个使用大语言模型&#xff08;LLM&#xff09;来决定应用程序控制流的系统。然而&#xff0c;智能体的定义并不唯一&#xff0c;不同人有不同的看法。Langchain的创始人Harrison Chase从技术角度给出了定义&#xff0c;但更…

数据库类型介绍

1. 关系型数据库&#xff08;RDBMS&#xff09; 关系型数据库是最常见的一类数据库&#xff0c;它们通过表&#xff08;Table&#xff09;来存储数据&#xff0c;表之间通过关系&#xff08;如主键和外键&#xff09;来关联。 • MySQL&#xff1a;开源的关系型数据库管理系统&…

【红帽Linux】简述Linux文件系统结构

原创 厦门微思网络 Linux 文件系统结构划分清晰、功能明确&#xff0c;每个目录都有特定的用途。以下是各个主要目录的介绍&#xff1a; n/bin: 包含系统启动和单用户模式下的基本命令的二进制文件&#xff0c;例如常见的基本命令 ls 和 cp。 n/boot: 保存与系统启动相关的文…

分贝通上线“在线比价”机制,帮助企业在差旅采购中持续获得低价资源

在企业差旅采购中,如何在不断波动的供求关系价格中保持相对价格优势,是企业进行成本管理必须面临的主要挑战之一。差旅平台分贝通通过其“单位降本”产品逻辑,在差旅管理中实现了显著的成本优化效果,帮助3000合作企业在高频支出场景中取得了可持续的低价优势。 差旅平台分贝通…

MySQL 如何用C语言连接

✨✨✨励志成为超级技术宅 ✨✨✨ 本文主要讲解在Linux服务器上&#xff0c;如何使用c语言MySQL库的接口来对MySQL数据库进行操作&#xff0c;如果没有服务器安装MySQL&#xff0c;也可以先学学看怎么用c语言mysql库的接口&#xff0c;还是比较容易的了。(●☌◡☌●)。那么开…

Hadoop生态圈框架部署(六)- HBase完全分布式部署

文章目录 前言一、Hbase完全分布式部署&#xff08;手动部署&#xff09;1. 下载Hbase2. 上传安装包3. 解压HBase安装包4. 配置HBase配置文件4.1 修改hbase-env.sh配置文件4.2 修改hbase-site.xml配置文件4.3 修改regionservers配置文件4.4 删除hbase中slf4j-reload4j-1.7.33.j…

OpenCV与AI深度学习 | 基于YoloV11自定义数据集实现车辆事故检测(有源码,建议收藏!)

本文来源公众号“OpenCV与AI深度学习”&#xff0c;仅用于学术分享&#xff0c;侵权删&#xff0c;干货满满。 原文链接&#xff1a;基于YoloV11自定义数据集实现车辆事故检测 在智能交通系统领域&#xff0c;实时检测车辆事故的能力变得越来越重要。该项目利用先进的计算机视…

Stable Diffusion 秋叶整合包:Deoldify 插件安装不上的处理办法

打开文件 install.py&#xff0c;参见下图&#xff1a; 把 fasiai 的版本号改成 1.0.61 即可。参见下图&#xff1a;

windows下qt5.12.11使用ODBC远程连接mysql数据库

1、下载并安装mysql驱动,下载地址:https://dev.mysql.com/downloads/ 2、配置ODBC数据源,打开64位的ODBC数据源配置工具:

7+纯生信,单细胞识别细胞marker+100种机器学习组合建模,机器学习组合建模取代单独lasso回归势在必行!

影响因子&#xff1a;7.3 研究概述&#xff1a; 皮肤黑色素瘤&#xff08;SKCM&#xff09;是所有皮肤恶性肿瘤中最具侵袭性的类型。本研究从GEO数据库下载单细胞RNA测序&#xff08;scRNA-seq&#xff09;数据集&#xff0c;根据原始研究中定义的细胞标记重新注释各种免疫细胞…

World of Warcraft [WeakAuras]Barney Raid Kit - Collapsing Star Indicator

https://wago.io/BarneyCS 黄色数字表示需要修的血量。 绿色数字表示停止修血。 红色数字表示修血过量&#xff0c;以及该坍缩星将在大爆炸读条结束前多少秒爆炸。 Numbers in yellow means damage required. Numbers in green means HP is good, dont damage anymore. Numbers…

丹摩征文活动 | 0基础带你上手经典目标检测模型 Faster-Rcnn

文章目录 &#x1f34b;1 引言&#x1f34b;2 平台优势&#x1f34b;3 丹摩平台服务器配置教程&#x1f34b;4 实操案例&#xff08; Faster-rcnn 项目&#xff09;&#x1f34b;4.1 文件处理&#x1f34b;4.2 环境配置&#x1f34b;4.3 训练模型&#x1f34b;4.4 数据保存并导…

17.UE5丰富怪物、结构体、数据表、构造函数

2-19 丰富怪物&#xff0c;结构体、数据表格、构造函数_哔哩哔哩_bilibili 目录 1.结构体和数据表格 2.在构造函数中初始化怪物 3.实现怪物是否游荡 1.结构体和数据表格 创建蓝图&#xff1a;结构体蓝图 在结构体蓝图中添加变量&#xff0c;如下所示&#xff0c;为了实现不…