【JUC】二十、volatile变量的特点与使用场景
文章目录
- 1、volatile可见性案例
- 2、线程工作内存与主内存之间的原子操作
- 3、volatile变量不具有原子性案例
- 4、无原子性的原因分析:i++
- 5、volatile变量小总结
- 6、重排序
- 7、volatile变量禁重排的案例
- 8、日常使用场景
- 9、总结
volatile变量的特点:
- 可见性
- 禁重排
- 无原子性
1、volatile可见性案例
volatile的可见性,即保证不同线程对某一个变量一旦完成更改,其他线程立即可见,因为会从线程的工作内存立马刷到主内存。Demo程序:
public class VolatileDemo1 {static boolean flag = true;public static void main(String[] args) throws InterruptedException {new Thread(() -> {System.out.println(Thread.currentThread().getName() + "come in ...");while (flag) {}System.out.println("flag被设置为false,线程任务执行结束");}, "t1").start();TimeUnit.SECONDS.sleep(2);flag = false;System.out.println(Thread.currentThread().getName() + "线程已将flag改为false");}
}
flag已被main线程改为false,但t1线程没有收到通知而一直在循环:
变量改为volatile变量:
static volatile boolean flag = true;
重新运行:
t1程序可正常停止了,原因就是加了volatile后,flag变量有了可见性,main线程改完后t1可以知道这个变更。
线程t1中为何看不到被主线程main修改为false的flag的值?
可能原因有:
- 主线程修改了flag之后没有将其刷新到主内存,所以t1线程看不到
- 主线程将flag刷新到了主内存,但是t1一直自娱自乐,读取的是自己工作内存中fag的值,没有去主内存中更新获取flag最新的值
想解决这个问题需要:
- 线程中修改了自己工作内存中的副本之后,立即将其刷新到主内存
- 工作内存中每次读取共享变量时,都去主内存中重新读取,然后拷贝到工作内存。
使用volatile修饰共享变量,就可以达到上面的效果,被volatile修改的变量有以下特点:
- 线程中读取的时候,每次读取都会去主内存中读取共享变量最新的值,然后将其复制到工作内存
- 线程中修改了工作内存中变量的副本,修改之后会立即刷新到主内存
2、线程工作内存与主内存之间的原子操作
-
read:作用于主内存,将变量的值从主内存传输到工作内存,主内存到工作内存
-
load:作用于工作内存,将read从主内存传输的变量值放入工作内存变量副本中,即数据加载
-
use:作用于工作内存,将工作内存变量副本的值传递给执行引擎,每当JVM遇到需要该变量的字节码指令时就会执行该操作
-
assign:作用于工作内存,将从执行引擎接收到的值赋值给工作内存变量,每当JVM遇到一个给变量赋值字节码指令时就会执行该操作
-
store:作用于工作内存,将赋值完毕的工作变量的值写回给主内存
-
write:作用于主内存,将store传输过来的变量值赋值给主内存中的变量
-
lock:作用于主内存,将一个变量标记为一个线程独占的状态,只是写时候加锁,就只是锁了写变量的过程
-
unlock:作用于主内存,把一个处于锁定状态的变量释放,然后才能被其他线程占用
以上面的flag变量为例,说明volatile变量的读写过程:
- step1:先从主内存中读到,然后load加载到线程t1自己的工作内存(read、load成对出现),开始use到while循环,然后一直循环
- step2:main线程同样的操作从主内存load到自己的工作内存,但它配合CPU完成了对flag的赋值(assign),并存储(store)到自己的工作内存
- step3:接下来main线程准备要把这个变更写回主内存了,此时必须加锁lock,写完后解锁unlock
- step4:上一步的加锁后会清空其他线程工作内存这个变量的值,在使用变量前必须重新load或者assign,因此t1线程可以获取到最新的变量值
3、volatile变量不具有原子性案例
volatile变量的复合操作不具有原子性,比如number++
先看不用volatile的:
class MyNumber{int number;public synchronized void addPlus(){number++;}
}
同时开十个线程,每个线程调用1000次addPlus方法:
public class VolatileDemo1 {public static void main(String[] args) throws InterruptedException {MyNumber myNumber = new MyNumber();for (int i = 0; i < 10; i++) {new Thread(() -> {for (int j = 0; j < 1000; j++) {myNumber.addPlus();}},String.valueOf(i)).start();}//给上面线程的计算时间Thread.sleep(2000);System.out.println(myNumber.number);}
}
正常输出10000,修改:不用synchronized,变量改为带volatile关键字的:
运行,接近10000,但不会等于10000:
4、无原子性的原因分析:i++
在没有加锁的控制时,就没有原子性的保证(synchronized依靠monitor来保证同一时间只能有一个线程来操作):当线程1对主内存对象发起read操作到write操作第一套流程的时间里,线程2随时都有可能对这个主内存对象发起第二套换作,如下图,虚线表示线程2的读取的可能时机:
从源代码来看,number++只有一行,但对应到底层则是:
而线程自己的工作内存里,数据加载、计算和赋值这三步,不是原子操作,是可能被分开的。
对于volatile变量的可见性,JVM只是保证从主内存加载到线程的工作内存的值是最新的,即数据加载这一步是最新的,对比上面的案例:主内存中,volatile修饰的变量number=5,此时线程A和线程B都能读,线程A要进行+1,线程B也要进行+1,但线程B在CPU的调度下一口气走完了数据加载、计算和赋值这三步,并刷回主内存,此时主内存number=6,而线程B比较慢,刚做完+1的计算,但由于volatile的可见性,主内存中已经等于6了,线程B的值作废,去主内存重读,number = 6,然后线程B一路number+1=7并刷到主内存,一看,两个线程,做了三次+1的操作,结果number只是从5变到7,这就是上面结果接近10000但小于10000的原因
由此可见volatile解决的是变量读时的可见性问题,但无法保证原子性,对于多线程修改主内存共享变量的场景必须使用加锁同步,加锁,同一时间,最多只能有一个线程进来,每次+1,都能走完写的整体流程,因为其他线程进不来,没有上面那种刚完成数据加载,然后数据就被别的线程改了并刷到主内存,导致自己刚加载的作废的情况。再从i++的字节码来看:
原子性指的是一个操作是不可中断的,即使是在多线程环境下,一个操作一旦开始就不会被其他线程影响。
很明显,i++是个复合操作,分了三步,不具备原子性。如果第二个线程在第一个线程读取旧值和新值写回期间读取了i的值,就会出现两个线程同时对一个值做加1的情况。比如i=6时,线程1完成了6+1,在写回新值前,线程2读取了数据,继续6+1,不管是线程1和线程2谁先写回主内存,(哪怕都写回主内存也是个7,何况volatile下,慢的那一个线程会去主内存重读),都是6+1做了两次,但最后等于7,相当于加了1次
5、volatile变量小总结
volatile变量不适合参与到依赖当前值的运算,比如i = i+1 ; i++之类的
依靠volatile变量的可见性,其适合用于保存某个状态的Boolean值
6、重排序
重排序是编译器和处理器为了优化程序性能而对指令序列进行重新排序的一种手段,但前提是不能改变原语义,或者说指令不能存在数据依赖关系,数据依赖关系即:若两个操作访问同一变量,且这两个操作中有一个为写操作,此时两操作间就存在数据依赖证。
若存在数据依赖关系,禁止重排序===> 重排序发生,会导致程序运行结果不同。
7、volatile变量禁重排的案例
Demo:
public class VolatileDemo2 {int i = 0;volatile boolean flag = false;public void write(){i = 2;flag = true;}public void read(){if(flag){System.out.println("---i = " + i);}}}
在每个volatile写操作后面插入写读屏障:
在每个volatile读操作后面插入读写屏障,
如此,程序原来的的语义就得到的保证
8、日常使用场景
Case1:单一赋值可以,但是含有复合运算赋值(如i++)不适用
volatile int a = 10;
Case2:在高并发里面,如果是靠变量来通知其他线程来改变后续动作的,那可利用volatile变量的可见性,做状态标志位,判断业务是否结束
volatile boolean flag = false;
做为一个布尔状态标志,判断业务是否该结束了
Case3:开销较低的读写锁策略
get和increment方法都加synchronized,安全性是保证了,但太重,性能下降太多:
public class Counter{private int vlaue;public synchronized int getValue(){ //读也得先拿对象锁return value;}public synchronized int incerment(){return value++;}
}
考虑synchronized结合volatile,此时,也可以每次都读到最新的数据,即使没加锁:
public class Counter{private volatile int vlaue; //volatilepublic int getValue(){ //利用volatile可见性保证并发下也能读取到最新值return value;}public synchronized int incerment(){ //利用synchronized保证复合操作的原子性return value++;}
}
Case4:DCL双端检查锁的禁重排
参考经典文章:这篇循序渐进,都讲明白了:
- 【单例模式下的DCL】
大概贴下代码:双端检查锁的普通代码:
问题:
重排序后,先给变量指向了分配的内存地址,在初始化对象前,多线程下,其他线程获取,判断对象是否为null,很明显,对象内存地址不为null了,但其实对象还没new,后面用它就会空指针:
需要给这个变量加volatile关键字来禁止指令重排:
Case5:对象属性修改原子类中,更新的对象属性必须使用 public volatile 修饰符
9、总结
凭什么java写了一个volatile关键字,就可以让系统底层加入内存屏障?两者关系怎么勾搭上的?
什么是内存屏障?
内存屏障是一种 屏障指令,它使得 CPU或编译器对屏障指令的前和后所发出的内存操作执行一个排序的约束 。 也叫内存栅栏或栅栏指令。
内存屏障能干嘛?
- 阻止屏障两边的指令重排序
- 写数据时加入屏障,强制将线程私有工作内存的数据刷回主物理内存
- 读数据时加入屏障,线程私有工作内存的数据失效,重新到主物理内存中获取最新数据
内存屏障的四大指令?
- 在每一个volatile写操作前面插入一个StoreStore屏障
- 在每一个volatile写操作后面插入一个StoreLoad屏障
- 在每一个volatile读操作后面插入一个LoadLoad屏障
- 在每一个volatile读操作后面插入一个LoadStore屏障
总之:volatile即可见性、禁重排、以及无原子性
- volatile 写之前的操作,都禁止重排序到 volatile 之后
- volatile 读之后的操作,都禁止重排序到 volatile 之前
- volatile 写之后 volatile 读,禁止重排序
相关文章:

【JUC】二十、volatile变量的特点与使用场景
文章目录 1、volatile可见性案例2、线程工作内存与主内存之间的原子操作3、volatile变量不具有原子性案例4、无原子性的原因分析:i5、volatile变量小总结6、重排序7、volatile变量禁重排的案例8、日常使用场景9、总结 volatile变量的特点: 可见性禁重排无…...
软件工程期末复习(2)
学习资料 设计模式与软件体系结构【期末全整理答案】_软件设计模式与体系结构期末考试题_鸽子不二的博客-CSDN博客 软件设计与体系结构(第二版)部分习题_软件设计与体系结构第二版课后答案-CSDN博客 软件体系结构试题库试题和答案 - 豆丁网Docin 软件设计与体系结构复习 - CN…...

[vue3] 使用 vite 创建vue3项目的详细流程
一、vite介绍 Vite(法语意为 “快速的”,发音 /vit/,发音同 “veet”) 是一种新型前端构建工具,能够显著提升前端开发体验(热更新、打包构建速度更快)。 二、使用vite构建项目 【学习指南】学习新技能最…...

#HarmonyOS:软件安装window和mac预览Hello World
Window软件地址 https://developer.harmonyos.com/cn/develop/deveco-studio#download 安装的建议 这个界面这样选,其他界面全部按照默认路径往下走!!! 等待安装… 安装环境错误处理 一般就是本地node配置异常导致ÿ…...
nginx 一键切换停机维护页面 —— 筑梦之路
背景说明 进行停机维护或者系统升级等操作,会影响到用户使用,如果停机维护期间用户未看到停机维护的通知,仍去访问系统,会提示默认不太友好的访问错误界面 ,这时如果在维护的时候直接展示停机公告的具体信息࿰…...
Python作业答疑
1. 旋转字符串 1.1 问题描述 给定一个字符串(以字符数组的形式)和一个偏移量,根据偏移量原地从左向右旋转字符串。 1.2 问题示例 输入str"abcdefg",offset3,输出"efgabcd"。 输入str"ab…...
计算机网络实用工具之Hydra
简介 Hydra 是一个并行登录破解程序,支持多种协议进行攻击。它非常快速且灵活,并且很容易添加新模块。 该工具使研究人员和安全顾问能够展示远程未经授权访问系统是多么容易。 目前该工具支持以下协议: Asterisk, AFP, Cisco AAA, Cisco au…...
AUTOSAR 入门
前言 AUTOSAR是什么Vector DaVinci 工具功能快捷键合理的创建标题,有助于目录的生成如何改变文本的样式插入链接与图片如何插入一段漂亮的代码片生成一个适合你的列表创建一个表格设定内容居中、居左、居右SmartyPants 创建一个自定义列表如何创建一个注脚注释也是必…...

新版IDEA中,module模块无法被识别,类全部变成咖啡杯无法被识
新版IDEA中,module模块无法被识别,类全部变成咖啡杯无法被识 如下图: 解决方法:java的Directory文件没有被设置为根目录,解决方法如下: 这是方法之一,还有很多的原因 可能的原因: …...

vue.js el-table 动态单元格列合并
一、业务需求: 一个展示列表,表格中有一部分列是根据后端接口动态展示,对于不同类型的数据展示效果不一样。如果接口返回数据是’类型1‘的,则正常展示,如果是’类型2‘的数据,则合并当前数据的动态表格。…...

word模板导出word文件
前期准备工作word模板 右键字段如果无编辑域 ctrlF9 一下,然后再右键 wps 直接 ctrlF9 会变成编辑域 pom.xml所需依赖 <dependencies> <!--word 依赖--> <dependency><groupId>fr.opensagres.xdocreport</groupId><artifactId…...
debianubuntu的nvidia驱动升级
背景 给出的机器的驱动版本不符合要求,使用自定义的驱动版本。 前置 如果使用nvidia官方的.run安装的驱动包,可以使用系统自带的nvidia-uninstall命令卸载比较方便,不建议使用apt pruge nvidia-*命令删除。会带来其他的问题。 卸载完成之…...

【开源视频联动物联网平台】视频接入网关的用法
视频接入网关是一种功能强大的视频网关设备,能够解决各种视频接入、视频输出、视频转码和视频融合等问题。它可以在应急指挥、智慧融合等项目中发挥重要作用,与各种系统进行对接,解决视频能力跨系统集成的难题。 很多视频接入网关在接入协议…...
【bug排查解决】现象级延迟8-10s
业务背景 最近公司在做物联网相关的项目,调试过程中发现好玩的bug。 首先一个数据采集场景,plc采集数据全链路: kepServer(kepserver IOT gateway) -> emqx (查看日志)-> iot服务 -> 业…...
【人生感悟】不能对一个人太好是有心理学原理的
1、不能对一个人太好是有心理学原理的,当你长期友善对待一个人时,如果这个人认知程度不是很高,层次稍微的偏低,那他可能直接把你的友善理解为理所应当,甚至是你在讨好他,还会把你们之间的关系理解成他是高于…...

动态规划学习——最长回文子序列,让字符串变成回文串的最小插入次数
一,最长回文串 1.题目 给你一个字符串 s ,找出其中最长的回文子序列,并返回该序列的长度。 子序列定义为:不改变剩余字符顺序的情况下,删除某些字符或者不删除任何字符形成的一个序列。 示例 1: 输入&…...
CSS新手入门笔记整理:CSS列表样式
列表项符号:list-style-type 在HTML中,对于有序列表和无序列表的列表项符号,都是使用type属性来定义的。 语法 list-style-type:取值; list-style-type属性是针对ol或者ul元素的,而不是li元素。 有序列表属性 属性值 说明 …...
12月07日,每日信息差
以下是2023年12月07日的11条信息差 第一、社交媒体公司X计划在日本成立应用开发团队 第二、造车进程加快,小米汽车在多地招聘零售门店主管,零售门店主管工作地点涉及武汉、重庆、长沙、郑州、佛山、东莞、厦门等城市 第三、我国西南地区首座百万千瓦级…...

spring mvc理解
spring mvc M:model 模型 V:view 视图 C:controller 控制器 S: service 服务处理 D: Dao 数据持久化 视图 我理解就是web页面,帮助用户调用后端接口。 前后端分离之后,view似乎就和后端没什么关系了。 模型 格式…...

HTML-标签之文字排版、图片、链接、音视频
1、标签语法 HTML超文本标记语言——HyperText Markup Language 超文本是链接标记也叫标签,带尖括号的文本 2、HTML基本骨架 HTML基本骨架是网页模板 html:整个网页head:网页头部,存放给浏览器看的代码,例如CSSbody…...

日语AI面试高效通关秘籍:专业解读与青柚面试智能助攻
在如今就业市场竞争日益激烈的背景下,越来越多的求职者将目光投向了日本及中日双语岗位。但是,一场日语面试往往让许多人感到步履维艰。你是否也曾因为面试官抛出的“刁钻问题”而心生畏惧?面对生疏的日语交流环境,即便提前恶补了…...
三维GIS开发cesium智慧地铁教程(5)Cesium相机控制
一、环境搭建 <script src"../cesium1.99/Build/Cesium/Cesium.js"></script> <link rel"stylesheet" href"../cesium1.99/Build/Cesium/Widgets/widgets.css"> 关键配置点: 路径验证:确保相对路径.…...

JavaScript 中的 ES|QL:利用 Apache Arrow 工具
作者:来自 Elastic Jeffrey Rengifo 学习如何将 ES|QL 与 JavaScript 的 Apache Arrow 客户端工具一起使用。 想获得 Elastic 认证吗?了解下一期 Elasticsearch Engineer 培训的时间吧! Elasticsearch 拥有众多新功能,助你为自己…...

练习(含atoi的模拟实现,自定义类型等练习)
一、结构体大小的计算及位段 (结构体大小计算及位段 详解请看:自定义类型:结构体进阶-CSDN博客) 1.在32位系统环境,编译选项为4字节对齐,那么sizeof(A)和sizeof(B)是多少? #pragma pack(4)st…...
在HarmonyOS ArkTS ArkUI-X 5.0及以上版本中,手势开发全攻略:
在 HarmonyOS 应用开发中,手势交互是连接用户与设备的核心纽带。ArkTS 框架提供了丰富的手势处理能力,既支持点击、长按、拖拽等基础单一手势的精细控制,也能通过多种绑定策略解决父子组件的手势竞争问题。本文将结合官方开发文档,…...

汽车生产虚拟实训中的技能提升与生产优化
在制造业蓬勃发展的大背景下,虚拟教学实训宛如一颗璀璨的新星,正发挥着不可或缺且日益凸显的关键作用,源源不断地为企业的稳健前行与创新发展注入磅礴强大的动力。就以汽车制造企业这一极具代表性的行业主体为例,汽车生产线上各类…...

全球首个30米分辨率湿地数据集(2000—2022)
数据简介 今天我们分享的数据是全球30米分辨率湿地数据集,包含8种湿地亚类,该数据以0.5X0.5的瓦片存储,我们整理了所有属于中国的瓦片名称与其对应省份,方便大家研究使用。 该数据集作为全球首个30米分辨率、覆盖2000–2022年时间…...
在 Nginx Stream 层“改写”MQTT ngx_stream_mqtt_filter_module
1、为什么要修改 CONNECT 报文? 多租户隔离:自动为接入设备追加租户前缀,后端按 ClientID 拆分队列。零代码鉴权:将入站用户名替换为 OAuth Access-Token,后端 Broker 统一校验。灰度发布:根据 IP/地理位写…...

【Post-process】【VBA】ETABS VBA FrameObj.GetNameList and write to EXCEL
ETABS API实战:导出框架元素数据到Excel 在结构工程师的日常工作中,经常需要从ETABS模型中提取框架元素信息进行后续分析。手动复制粘贴不仅耗时,还容易出错。今天我们来用简单的VBA代码实现自动化导出。 🎯 我们要实现什么? 一键点击,就能将ETABS中所有框架元素的基…...

ui框架-文件列表展示
ui框架-文件列表展示 介绍 UI框架的文件列表展示组件,可以展示文件夹,支持列表展示和图标展示模式。组件提供了丰富的功能和可配置选项,适用于文件管理、文件上传等场景。 功能特性 支持列表模式和网格模式的切换展示支持文件和文件夹的层…...