深度解析volatile关键字,就是这么简单
点击上方 "程序员小乐"关注, 星标或置顶一起成长
后台回复“大礼包”有惊喜礼包!
关注订阅号「程序员小乐」,收看更多精彩内容
每日英文
Sometimes, you don't get over things. You just learn to live with the pain.-有时候,我们并非走出了伤痛,不过是学会了带着伤痛继续生活。
每日掏心话
有时候你把什么放下了,不是因为突然就舍得了,是因为期限到了,任性够了,成熟多了,也就知道这一页该翻过去了。
来自:谭嘉俊 | 责编:乐乐
链接:juejin.im/user/2400989124522446
程序员小乐(ID:study_tech)第 1036 次推文
往日回顾:快手公司厕所装坑位计时器,网友:再也不能带薪拉屎了!
正文
/ 开始 /
本文章讲解的内容是深入了解volatile关键字,建议对着示例项目阅读文章,示例项目链接如下:
VolatileDemo
https://github.com/TanJiaJunBeyond/VolatileDemo
查看汇编代码的hsdis-amd64.dylib文件链接如下:
hsdis-amd64.dylib
https://github.com/TanJiaJunBeyond/VolatileDemo/blob/master/hsdis-amd64.dylib
关键字volatile是Java虚拟机提供的最轻量级的同步机制,当一个变量被关键字volatile修饰之后,它有如下两个特性:
保证了这个变量对所有线程的可见性
禁止指令重排序优化
/ 保证变量对所有线程的可见性 /
关键字volatile可以保证变量对所有线程的可见性,也就是当一个线程修改了这个变量的值,其他线程能够立即得到修改的值。普通变量是做不到这样,普通变量的值需要通过主内存在线程之间传递。
举个例子:线程A修改一个普通变量的值,然后传送给主内存,另外一个线程B需要等到传送完主内存后才能够从主内存进行读取操作,这样变量最新的值才会对线程B可见。先看下如下例子,代码如下所示:
/*** Created by TanJiaJun on 2020-08-16.*/
class VolatileDemo {private static final int THREADS_COUNT = 10;private static volatile int value = 0;private static void increase() {// 对value变量进行自增操作value++;}public static void main(String[] args) {// 创建10个线程Thread[] threads = new Thread[THREADS_COUNT];for (int i = 0; i < THREADS_COUNT; i++) {threads[i] = new Thread(() -> {for (int j = 0; j < 1000; j++)// 每个线程对value变量进行1000次自增操作increase();});threads[i].start();}// 主线程等待子线程运行结束for (Thread thread : threads) {try {thread.join();} catch (InterruptedException e) {e.printStackTrace();}}System.out.println("value的值:" + value);}}
这段代码的意思是发起10个线程,然后每个线程对value变量进行1000次自增操作,如果这段代码正确地并发操作,最后的结果value的值应该是10000,但是实际上多次运行后,value的值都是小于等于10000的值。
搜索公众号程序员小乐回复关键字“Java”,获取Java面试题和答案。
这段代码中increase方法调用i++,也就是i = i + 1,它不是原子性操作,Java内存模型直接保证的原子性变量操作包括read、load、assign、use、store和write,我们可以认为基本数据类型的读写都具备原子性,有个例外就是long和double的非原子性协定,不过我们无须太过在意,虽然Java内存模型允许虚拟机不把long和double的变量的读写实现为原子性操作。
但是现在的商用虚拟机都几乎把这些操作实现为原子性操作,原子性操作是指执行一系列操作,这些操作要么全部执行,要么全部不执行,不存在只执行其中一部分的情况,举个例子:i = 1就是个原子性操作,但是i = i + 1就不是原子性操作,因为这个操作是由多条字节码指令构成的,我用Javap反编译上面的示例代码,先找到生成的Class文件,路径是
/Users/tanjiajun/IdeaProjects/VolatileDemo/out/production/VolatileDemo/VolatileDemo.class
就是在VolatileDemo目录下的out文件夹中,然后执行javap -p -v VolatileDemo命令,生成如下字节码:
(由于源码过长,想详细了解的可以到原文章阅读)
然后找到对应的increase方法的字节码,字节码如下所示:
private static void increase();descriptor: ()Vflags: (0x000a) ACC_PRIVATE, ACC_STATICCode:stack=2, locals=0, args_size=00: getstatic #7 // Field value:I3: iconst_14: iadd5: putstatic #7 // Field value:I8: returnLineNumberTable:line 12: 0line 13: 8
可以看到value++是由四条指令构成的,分别是getstatic、iconst_1、iadd和putstatic,getstatic指令是获取静态字段value的值并且放入操作栈顶,iconst_1指令是把常量1放入操作栈顶,iadd指令是把当前操作栈顶中两个值相加并且把结果放入操作栈顶,putstatic指令是把操作栈顶的结果赋值给静态变量value,关键字volatile可以保证执行getstatic指令后的值是正确的。
如果在并发环境下,可能有其他线程在执行iconst_1指令或者iadd指令时,增加了value的值,导致操作栈顶的值就变成了过期的数据,在执行putstatic指令后可能把较小的value的值同步回主内存中,导致不能得到正确的结果。
从上面的例子可以得知,volatile变量只保证可见性,以下两条规则的运算环境可以保证这些操作的原子性:
只有单条线程修改变量的值,运算结果不依赖变量当前的值,也就是说不依赖产生的中间结果。
变量不需要与其他的状态变量共同参与不变约束。
如果不符合以上两条规则的话,就需要通过加锁来保证这些操作的原子性,可以使用关键字synchronized或者java.util.concurrent中的原子类。
/ 禁止指令重排序优化 /
Java内存模型中的一个语义是线程内表现为串行的语义(Within-Thread As-If-Serial Semantics),它是指普通变量只能保证在该方法在执行过程中所有依赖赋值结果的地方都能得到正确的结果,但是不保证变量的赋值操作的顺序和程序代码中的执行顺序是一致的。举个例子,代码如下所示:
int i = 1;
int j = 2;
int k = i + j;
上面这段代码大概执行了以下步骤:
将常量1赋值给i
将常量2赋值给j
取到i的值
取到j的值
将i的值和j的值相加后赋值给k
在上面这五个步骤中,步骤1可能会和步骤2和步骤4重排序,步骤2可能会和步骤1和步骤3重排序,步骤3可能会和步骤2和步骤4重排序,步骤4可能会和步骤1和步骤3重排序,但是步骤1、步骤3和步骤5之间不能重排序,步骤2、步骤4和步骤5之间不能重排序,因为它们之间存在依赖关系,一旦重排序,线程表现为串行的语义将无法得到保证。
再看个例子,使用双重检查锁定(DCL)实现单例模式,代码如下所示:
/*** Created by TanJiaJun on 2020/8/23.*/
class Singleton {// 用关键字volatile修饰变量sInstance,禁止指令重排序优化private static volatile Singleton sInstance;// 私有构造方法private Singleton() {// 防止通过反射调用构造方法导致单例失效if (sInstance != null)throw new RuntimeException("Cannot construct a singleton more than once.");}// 获取单例的方法public static Singleton getInstance() {// 第一次判断sInstance是否为空,用于判断是否需要同步,提高性能和效率if (sInstance == null) {// 使用synchronized修饰代码块,取Singleton的Class对象作为锁对象synchronized (Singleton.class) {// 第二次判断sInstance是否为空,用于判断是否已经创建实例if (sInstance == null) {// 创建Singleton对象sInstance = new Singleton();}}}// 返回sInstancereturn sInstance;}public static void main(String[] args) {Singleton.getInstance();}}
然后使用HSDIS插件反汇编上面的代码,我只截取了对变量sInstance赋值(第25行)的那部分汇编代码,如果想要看全部的汇编代码,可以在查看SingletonAssemblyCodeWithVolatile.log,汇编代码如下所示:
0x000000011b33f4c7: mov 0x38(%rsp),%rax0x000000011b33f4cc: movabs $0x61ff0ac48,%rdx ; {oop(a 'java/lang/Class'{0x000000061ff0ac48} = 'Singleton')}0x000000011b33f4d6: movsbl 0x30(%r15),%esi0x000000011b33f4db: cmp $0x0,%esi0x000000011b33f4de: jne 0x000000011b33f6e90x000000011b33f4e4: mov %rax,%r100x000000011b33f4e7: shr $0x3,%r100x000000011b33f4eb: mov %r10d,0x70(%rdx)0x000000011b33f4ef: lock addl $0x0,-0x40(%rsp)0x000000011b33f4f5: mov %rdx,%rsi0x000000011b33f4f8: xor %rax,%rsi0x000000011b33f4fb: shr $0x15,%rsi0x000000011b33f4ff: cmp $0x0,%rsi0x000000011b33f503: jne 0x000000011b33f708 ;*putstatic sInstance {reexecute=0 rethrow=0 return_oop=0}; - Singleton::getInstance@24 (line 25)
然后把代码中的关键字volatile去掉,再生成汇编代码,我只截取了对变量sInstance赋值(第25行)的那部分汇编代码,如果想要看全部的汇编代码,可以在查看SingletonAssemblyCodeWithNoVolatile.log,汇编代码如下所示:
搜索公众号程序员小乐回复关键字“offer”,获取算法面试题和答案。
0x0000000116f2a4c7: mov 0x38(%rsp),%rax0x0000000116f2a4cc: movabs $0x61ff0acb8,%rdx ; {oop(a 'java/lang/Class'{0x000000061ff0acb8} = 'Singleton')}0x0000000116f2a4d6: movsbl 0x30(%r15),%esi0x0000000116f2a4db: cmp $0x0,%esi0x0000000116f2a4de: jne 0x0000000116f2a6e10x0000000116f2a4e4: mov %rax,%r100x0000000116f2a4e7: shr $0x3,%r100x0000000116f2a4eb: mov %r10d,0x70(%rdx)0x0000000116f2a4ef: mov %rdx,%rsi0x0000000116f2a4f2: xor %rax,%rsi0x0000000116f2a4f5: shr $0x15,%rsi0x0000000116f2a4f9: cmp $0x0,%rsi0x0000000116f2a4fd: jne 0x0000000116f2a700 ;*putstatic sInstance {reexecute=0 rethrow=0 return_oop=0}; - Singleton::getInstance@24 (line 25)
通过对比可以发现,如果变量sInstance被关键字volatile修饰,会在赋值(mov %r10d,0x70(%rdx))后多执行一个lock addl $0x0,-0x40(%rsp)指令,这个指令是一个内存屏障(Memory Barrier),它可以使内存屏障前的指令和内存屏障后的指令不会因为系统优化而导致乱序执行,后面会详细讲解,lock addl $0x0,-0x40(%rsp)(%rsp是堆栈指针寄存器,通常会指向栈顶位置,堆栈的pop操作和push操作是通过改变%rsp的值来移动堆栈指针的位置来实现)是一个空操作。
查询IA32手册可得知,使用这个空操作,而不是使用空操作指令nop是因为前缀lock不允许配合nop指令使用,其中前缀lock,查询IA32手册可得知,它的作用是使得本CPU的缓存写入内存,相当于对缓存中的变量执行store操作和write操作,这个写入动作可以让其他CPU或者别的内核无效化(Invalidata)其缓存,可以让前面对被关键字volatile修饰的变量的修改对其他线程立即可见。
/ 内存屏障 /
内存屏障(Memory Barrier),也称为内存栅栏、内存栅障和屏障指令等,是一类同步屏障指令,它使得CPU或者编译器在对内存进行操作的时候,严格按照一定的顺序执行,大多数现代计算机为了提高性能而采用乱序执行,它就可以使内存屏障前的指令和内存屏障后的指令不会因为系统优化而导致乱序执行。
内存屏障的语义是内存屏障前的所有写操作都要写入内存,内存屏障后的所有读操作都可以获得同步屏障之前的读操作的结果。
内存屏障可以分为以下四种类型:
LoadLoad屏障
序列:①Load1②LoadLoad③Load2
确保Load1要载入的数据能够在被Load2和后面的load指令载入数据前载入。
StoreStore屏障
序列:①Store1②StoreStore③Store2
确保Store1要存储的数据能够在Store2和后面的store指令同步回主内存前对其它处理器可见。
LoadStore屏障
序列:①Load1②LoadStore③Store2
确保Load1要载入的数据能够在Store2和后面的store指令同步回主内存前载入。
StoreLoad屏障
序列:①Store1②StoreLoad③Load2
确保Store1要存储的数据能够在Load2和后面的load指令载入数据前对其它处理器可见。它是这四种内存屏障中开销最大的,它也是一个万能屏障,具有其它三种内存屏障的功能。
下图展示了这些内存屏障如何符合JSR-133排序规则:
举个例子,代码如下所示:
/*** Created by TanJiaJun on 2020/8/23.*/
class MemoryBarrierTest {private int a, b;private volatile int c, d;private void test() {int i, j;i = a; // load aj = b; // load bi = c; // load c// LoadLoadj = d; // load d// LoadStorea = i; // store ab = j; // store b// StoreStorec = i; // store c// StoreStored = j; // store d// StoreLoadi = d; // load d// LoadLoad// LoadStorej = b; // load ba = i; // store a}}
另外,为了保证关键字final的特殊语义,会在下面的序列中加入内存屏障:
①x.finalField = v;②StoreStore③sharedRef = x;
/ 总结 /
总结下Java内存模型中对被关键字volatile修饰的变量进行read(读取)、load(载入)、use(使用)、assign(赋值)、store(存储)和write(写入)操作定义的特殊规则:
假设有一个线程A,有一个被关键字volatile修饰的变量i;只有当线程A对变量i执行的前一个操作是load操作的时候,线程A才能对变量i进行use操作;并且,只有线程A对变量i执行的后一个操作是use操作的时候,线程A才能对变量i执行load操作,也就是说,线程A对变量i执行use操作是和对其执行read操作和load操作相关联的,它们都必须要连续一起出现。
这条规则要求在工作内存中,每次使用volatile变量都必须从主内存中刷新最新的值,用于保证能看见其他线程对volatile变量的修改后的值。
假设有一个线程A,有一个被关键字volatile修饰的变量i;只有当线程A对变量i执行的前一个操作是assign操作的时候,才能对其进行store操作;并且,只有线程A对变量i执行后一个操作是store操作的时候,线程A才能对变量i进行assign操作,也就是说,线程A对变量i执行assign操作是和对其执行store操作和write操作相关联的,它们都必须要连续一起出现。
这条规则要求在工作内存中,每次修改volatile变量时都要立刻同步回主内存,用于保证其他线程能看见volatile变量修改后的值。
假设有一个线程A,有两个被关键字volatile修饰的变量,分别为i和j;假定动作A是线程A对volatile变量i执行use操作或者assign操作,假定动作B是和动作A相关联的load操作或者store操作,假定动作C是和动作B相关联的read操作或者write操作;假定动作D是线程A对volatile变量j执行use操作或者assign操作,假定动作E是和动作D相关联的load操作或者store操作,假定动作F是和动作E相关联的read操作或者write操作;如果动作A先于动作D,那么动作C先于动作F。
这条规则要求被关键字volatile修饰的变量不会被指令重排序优化,保证了代码的执行顺序和程序的顺序相同。
欢迎在留言区留下你的观点,一起讨论提高。如果今天的文章让你有新的启发,欢迎转发分享给更多人。欢迎加入程序员小乐技术交流群,在后台回复“加群”或者“学习”即可。
猜你还想看
阿里、腾讯、百度、华为、京东最新面试题汇集
delete后加 limit是个好习惯么 !
Centos7搭建k8s环境教程,一次性成功,收藏了!
还在用if(obj!=null)做非空判断?带你快速上手Optional实战性理解!
嘿,你在看吗?
深度解析volatile关键字,就是这么简单相关推荐
- java内存 海子_Java并发编程:从根源上解析volatile关键字的实现
Java并发编程:volatile关键字解析 1.解析概览 内存模型的相关概念 并发编程中的三个概念 Java内存模型 深入剖析volatile关键字 使用volatile关键字的场景 2.内存模型的 ...
- 以两种异步模型应用案例,深度解析Future接口
摘要:本文以实际案例的形式分析了两种异步模型,并从源码角度深度解析Future接口和FutureTask类. 本文分享自华为云社区<[精通高并发系列]两种异步模型与深度解析Future接口(一) ...
- 内存栅栏和volatile关键字
内存栅栏和volatile关键字 前言 本次主要讲解关于内存栅栏的一点小东西,主要是扫盲,给大家普及普及概念性的东西.以前我们说过在一些简单的案例中,比如一个字段赋值或递增该字段,我们需要对线程进行同 ...
- 慎重使用volatile关键字
volatile关键字相信了解Java多线程的读者都很清楚它的作用.volatile关键字用于声明简单类型变量,如int.float.boolean等数据类型.如果这些简单数据类型声明为volatil ...
- volatile关键字——保证并发编程中的可见性、有序性
文章目录 一.缓存一致性问题 二.并发编程中的三个概念 三.Java线程内存模型 1.原子性 2.可见性 3.有序性 四.深入剖析volatile关键字 1.volatile关键字的两层语义 2.vo ...
- Java并发编程 Volatile关键字解析
volatile关键字的两层语义 一旦一个共享变量(类的成员变量.类的静态成员变量)被volatile修饰之后,那么就具备了两层语义: 1)保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了 ...
- Java并发编程:volatile关键字解析(转载)
转自https://www.cnblogs.com/dolphin0520/p/3920373.html Java并发编程:volatile关键字解析 Java并发编程:volatile关键字解析 v ...
- 【Java并发编程:volatile关键字之解析】
Java并发编程:volatile关键字解析 - Matrix海子 - 博客园 在Java 5之前,volatile是一个备受争议的关键字:因为在程序中使用它往往会导致出人意料的结果.在Java 5之 ...
- [转] volatile关键字解析
摘要: 在 Java 并发编程中,要想使并发程序能够正确地执行,必须要保证三条原则,即:原子性.可见性和有序性.只要有一条原则没有被保证,就有可能会导致程序运行不正确.volatile关键字 被用来保 ...
- 理解Java并发编程:volatile关键字解析
文章目录 volatile关键字作用详解 原子/可见/有序 happen-before原则 volatile的作用 volatile的原理 volatile关键字作用详解 讲到Java中的volati ...
最新文章
- selenium之 chromedriver与chrome版本映射表(更新至v2.33)
- 阿里巴巴云原生应用安全防护实践与 OpenKruise 的新领域
- wxWidgets:wxSashWindow类用法
- WinPcap 获取本地适配器信息
- mysql如何快速插入一千万条数据_如何快速安全的插入千万条数据?
- ortp流媒体协议 [1]
- webdriver原理(自己做个记录)
- 前端开发中一些常用技巧总结
- linux sftp 增加用户(centos)
- Layui 数据表格复杂表头
- 视频教程-思科CCNP专题系列③:OSPF路由协议-思科认证
- RabbitMQ安装问题
- JAVA系统蓝屏_Tomcat启动系统蓝屏
- 程序员的成长之路——道和术的思考
- android 图片处理过程中添加进度条,[Android] 随时拍图像处理部分总结及源码分......
- vue 监听输入法方法(js)
- eclipse SVN A conflict in the working copy obstructs the current operation
- 蒲月“登高”,临风眺望,旷视邀您共赴AI的下一个十年之约
- 听书是怎样的一种体验
- 基于nginx搭建在线播放mp4
热门文章
- ES笔记_转自尚硅谷_其中有JAVA操作_ES
- Coursera | 免费上Coursera-助学金申请流程
- windows11没有ie浏览器解决办法
- 李沐论文精读系列二:Vision Transformer、MAE、Swin-Transformer
- sql server 2012 KB2716442安装错误解决方案(错误代码 0x84B20001)
- 计算机无法找到输出设备,老司机搞定win10声音无法找到输入输出设置的解决方法...
- Android 版本4.12 微信,安卓4.12微信下载
- A40i 平台应用笔记-华为-ME909S-4G 模块的移植应用
- Win32开发笔记(一):整体流程
- pe和linux一起安装到移动硬盘,能否把winpe安装到移动硬盘上