您好!
欢迎来到京东云开发者社区
登录
首页
博文
课程
大赛
工具
用户中心
开源
首页
博文
课程
大赛
工具
开源
更多
用户中心
开发者社区
>
博文
>
volatile禁止指令重排序探究
分享
打开微信扫码分享
点击前往QQ分享
点击前往微博分享
点击复制链接
volatile禁止指令重排序探究
自猿其说Tech
2021-09-27
IP归属:未知
38000浏览
计算机编程
### 1 引言 上篇文章我们介绍了volatile是如何保证内存中共享变量的可见性的,熟悉并发变成的同学可能都知道,volatile的另一大作用就是禁止指令重排序。下文会从cpu指令和JMM内存模型角度继续探究,volatile禁止指令重排序是怎么一回事儿。 ### 2 何为指令重排序 我们要搞清楚volatile如何禁止指令重排序,首先我们要明白何为指令重排序,指令重排序是怎么发生的。在volatile保证内存可见性一文中我们介绍过cpu存储器层次结构,了解了现在的计算机读取数据并不是从内存中直接读取,而读取顺序的优先级是:寄存器>高速缓存>内存。 随着cpu主频越来越高,与高速缓存交互的次数也越来越多,当cpu的速度远超过访问的cache时,会出现catche wait,过多的catche wait会产生性能瓶颈,为了避免这种情况的发生,处理器和编译器和为了优化程序性能而对指令序列进行重新排序,从而减少catche wait的产生。所以,总的来说重排序是指编译器和处理器为了优化程序性能而对指令序列进行重新排序的一种手段。 ### 3 重排序的类型 执行程序时,为了提高性能,编译器和处理器的常常会对指令做重排,一般分以下3种 #### 3.1 编译器优化的重排 编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序。 #### 3.2 指令并行的重排 现代处理器采用了指令级并行技术来将多条指令重叠执行。如果不存在数据依赖性(即后一个执行的语句无需依赖前面执行的语句的结果),处理器可以改变语句对应的机器指令的执行顺序 #### 3.3 内存系统的重排 由于处理器使用缓存和读写缓存冲区,这使得load和store操作看上去可能是在乱序执行,因为三级缓存的存在,导致内存与缓存的数据同步存在时间差。 下面我们就来用一个例子带大家来看一下编译重排序对程序的影响 ```java package com.example; public class VolatileExample2 { static int x = 0, y = 0; static int a = 0, b = 0; public static void main(String[] args) throws InterruptedException { for (int i = 0; i <= 100000; i++) { a = 0; b = 0; x = 0; y = 0; Thread thread1 = new Thread(() -> { a = 1; x = b; }); Thread thread2 = new Thread(() -> { b = 1; y = a; }); thread1.start(); thread2.start(); thread1.join(); thread2.join(); System.out.println("(" + x + "," + y + ")" + i + "次"); } } } ``` 我们来分析一下这段代码,最终结果可能会有(0,1),(1,0),(1,1)因为线程1,和2谁先执行并不一定,那么大家想一下,有没有可能出现(0,0)这种情况呢?如果出现了(0,0)这种情况又说明了什么呢? ![](//img1.jcloudcs.com/developer.jdcloud.com/9f758fb8-2261-41a7-8958-5089af9b8ad320210927144029.png) 如图,很幸运,我再尝试了N轮运行之后,终于出现了我们之前猜测的(0,0)这种情况,为什么会出现这种情况呢?这种情况的出现,应该下面这种场景。 ![](//img1.jcloudcs.com/developer.jdcloud.com/656b75ea-53b7-4a8a-875b-847a27e6fb4120210927144054.png) 如上图,如果线程1和线程2交替执行,且按照这个顺序是不是就可以完美解释为啥会出现(0,0)这种情况了。但是细心的同学可能发现了,这个顺序貌似不太对啊,线程1不是应该a=1 在前吗?同样线程2不应该是b=1在前吗?因为,在实际运行时,代码指令可能并不是严格按照代码语句顺序执行的,这种情况的出现说明发生了发生了指令“重排序”(reordering)。 ### 4 双重检查锁变量未用volatile修饰的隐患 上面例证明了指令重排序的存在,现在再给大家提过一个经典的场景和例子来给大家解释一下,volatile是如何禁止指令重排序的。 ```java package com.example; public class Singleton { private static Singleton instance = null; private Singleton() { } public static Singleton getInstance() { if (instance == null) { synchronized (Singleton.class) { if (instance == null) { instance = new Singleton(); } } } return instance ; } } ``` 大家看这段代码,看似没有什么问题,实则大家细看还是可以发现,Singleton 对象如果不用volatile进行修饰,那么并发量大的时候会发生什么问题呢?分析这段代码,它的问题出在instance =new Singleton() 我们都知道 这不是一个原子操作,在jvm这行代码,做了三个事情。 1. 给instance分配内存。 1. 调用Singleton的构造方法初始化成员变量。 1. 将instance变量指向分配好的内存空间。 只有在第三步执行完毕之后instance对象才会!=null。由于2 和3没有对应的以来关系,所以在jvm的即时编译器中,2和3的顺序是无法保证的,也就是说最终执行顺序可以是1-2-3,也可以是1-3-2.如果是1-3-2的顺序,那么在3执行完成之后也就是说instance!=null,此时2未执行,也就是说对象还未初始化,那么这个时候另一个线程使用这个instance对象会完美的报错。在解释这些问题之前,我们先来了解一些概念。 #### 4.1 happens-before 从JDK5开始,java使用新的JSR -133内存模型(本文除非特别说明,针对的都是JSR- 133内存模型)。JSR-133使用happens-before的概念来阐述操作之间的内存可见性。在JMM中,如果一个操作执行的结果需要对另一个操作可见,那么这两个操作之间必须要存在happens-before关系。这里提到的两个操作既可以是在一个线程之内,也可以是在不同线程之间。 与程序员密切相关的happens-before规则如下: 1. 程序顺序规则:一个线程中的每个操作,happens- before 于该线程中的任意后续操作。 1. 监视器锁规则:对一个监视器锁的解锁,happens- before 于随后对这个监视器锁的加锁。 1. volatile变量规则:对一个volatile域的写,happens- before 于任意后续对这个volatile域的读。 1. 传递性:如果A happens- before B,且B happens- before C,那么A happens- before C。 #### 4.2 volatile内存语义的实现 为了实现volatile的内存语义,编译器在生成字节码时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序。对于编译器来说,发现一个最优布置来最小化插入屏障的总数几乎不可能,为此,JMM采取保守策略。下面是基于保守策略的JMM内存屏障插入策略: - 在每个volatile写操作的前面插入一个StoreStore屏障。 - 在每个volatile写操作的后面插入一个StoreLoad屏障。 - 在每个volatile读操作的后面插入一个LoadLoad屏障。 - 在每个volatile读操作的后面插入一个LoadStore屏障。 了解了以上概念以后,我们再回来看一下。 ![](//img1.jcloudcs.com/developer.jdcloud.com/2df37a79-b9d0-4948-8cf4-2a3f448ef97c20210927144236.png) 此处我们再次对这段代码进行反汇编,我们可以看出。在赋值前多执行了一个“lock addl $0x0,(%rsp)”操作,这个操作的作用相当于是一个内存屏障,就是我们上面说的。 那为什么说它可以禁止指令重排呢?指令重排我们了解,从硬件架构上讲,指令重排序是指处理器采用了允许将多条指令不按程序规定的顺序分开发送给各个相应的电路单元进行处理。但并不是说指令任意重排,处理器必须能正确处理指令依赖的情况保障程序能得出正确的执行结果。如果一个指令依赖另一个指令,那么他们之间的顺序是不能够重排序的,所以在一个处理器中,重排序过的代码看起来依然是有序的。因此,lock addl $0x0,(%rsp)指令吧修改同步到内存时,意味着所有之前的操作都已经执行完成。所以说,volatile可以禁止指令的重排序! ### 5 总结 本文浅析了volatile是如何禁止指令重排序的,同时也简单介绍了重排序,以及重排序发生的场景,我所讲的这些内容参考了《java并发编程的艺术》和《深入理解java虚拟机》第三版,同时也加入了一些自己的思考当然,这些内容还只是并发编程的冰山一角,如果有错误或遗漏的地方欢迎各位同学一起探讨大家一起学习,一起进步。 ------------ ###### 自猿其说Tech-JDL京东物流技术发展部 ###### 作者:客户服务技术部 王子源
原创文章,需联系作者,授权转载
上一篇:Python-Scrapy爬虫实战
下一篇:通过keepalived保证nginx高可用
相关文章
Taro小程序跨端开发入门实战
Flutter For Web实践
配运基础数据缓存瘦身实践
自猿其说Tech
文章数
426
阅读量
2149964
作者其他文章
01
深入JDK中的Optional
本文将从Optional所解决的问题开始,逐层解剖,由浅入深,文中会出现Optioanl方法之间的对比,实践,误用情况分析,优缺点等。与大家一起,对这项Java8中的新特性,进行理解和深入。
01
Taro小程序跨端开发入门实战
为了让小程序开发更简单,更高效,我们采用 Taro 作为首选框架,我们将使用 Taro 的实践经验整理了出来,主要内容围绕着什么是 Taro,为什么用 Taro,以及 Taro 如何使用(正确使用的姿势),还有 Taro 背后的一些设计思想来进行展开,让大家能够对 Taro 有个完整的认识。
01
Flutter For Web实践
Flutter For Web 已经发布一年多时间,它的发布意味着我们可以真正地使用一套代码、一套资源部署整个大前端系统(包括:iOS、Android、Web)。渠道研发组经过一段时间的探索,使用Flutter For Web技术开发了移动端可视化编程平台—Flutter乐高,在这里希望和大家分享下使用Flutter For Web实践过程和踩坑实践
01
配运基础数据缓存瘦身实践
在基础数据的常规能力当中,数据的存取是最基础也是最重要的能力,为了整体提高数据的读取能力,缓存技术在基础数据的场景中得到了广泛的使用,下面会重点展示一下配运组近期针对数据缓存做的瘦身实践。
自猿其说Tech
文章数
426
阅读量
2149964
作者其他文章
01
深入JDK中的Optional
01
Taro小程序跨端开发入门实战
01
Flutter For Web实践
01
配运基础数据缓存瘦身实践
添加企业微信
获取1V1专业服务
扫码关注
京东云开发者公众号