java volatile内存屏障_从汇编看Volatile的内存屏障
Java的Volatile的特征是任何读都能读到最新值,本质上是JVM通过内存屏障来实现的,让我们看看从字节码以及汇编码的角度,来看下是否真是如此?
一 Volatile与内存屏障
为了实现volatile内存语义,JMM会分别限制重排序类型。下面是JMM针对编译器制定的volatile重排序规则表:
举例来说,第三行最后一个单元格的意思是:在程序顺序中,当第一个操作为普通变量的读或写时,如果第二个操作为volatile写,则编译器不能重排序这两个操作。
从上表我们可以看出:
当第二个操作是volatile写时,不管第一个操作是什么,都不能重排序。这个规则确保volatile写之前的操作不会被编译器重排序到volatile写之后。
当第一个操作是volatile读时,不管第二个操作是什么,都不能重排序。这个规则确保volatile读之后的操作不会被编译器重排序到volatile读之前。
当第一个操作是volatile写,第二个操作是volatile读时,不能重排序。
为了实现volatile的内存语义,编译器在生成字节码时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序。对于编译器来说,发现一个最优布置来最小化插入屏障的总数几乎不可能,为此,JMM采取保守策略。下面是基于保守策略的JMM内存屏障插入策略:
在每个volatile写操作的前面插入一个StoreStore屏障。
在每个volatile写操作的后面插入一个StoreLoad屏障。
在每个volatile读操作的后面插入一个LoadLoad屏障。
在每个volatile读操作的后面插入一个LoadStore屏障。
上述内存屏障插入策略非常保守,但它可以保证在任意处理器平台,任意的程序中都能得到正确的volatile内存语义。
下面是保守策略下,volatile写插入内存屏障后生成的指令序列示意图:
上图中的StoreStore屏障可以保证在volatile写之前,其前面的所有普通写操作已经对任意处理器可见了。这是因为StoreStore屏障将保障上面所有的普通写在volatile写之前刷新到主内存。
这里比较有意思的是volatile写后面的StoreLoad屏障。这个屏障的作用是避免volatile写与后面可能有的volatile读/写操作重排序。因为编译器常常无法准确判断在一个volatile写的后面,是否需要插入一个StoreLoad屏障(比如,一个volatile写之后方法立即return)。为了保证能正确实现volatile的内存语义,JMM在这里采取了保守策略:在每个volatile写的后面或在每个volatile读的前面插入一个StoreLoad屏障。从整体执行效率的角度考虑,JMM选择了在每个volatile写的后面插入一个StoreLoad屏障。因为volatile写-读内存语义的常见使用模式是:一个写线程写volatile变量,多个读线程读同一个volatile变量。当读线程的数量大大超过写线程时,选择在volatile写之后插入StoreLoad屏障将带来可观的执行效率的提升。从这里我们可以看到JMM在实现上的一个特点:首先确保正确性,然后再去追求执行效率。
下面是在保守策略下,volatile读插入内存屏障后生成的指令序列示意图:
上图中的LoadLoad屏障用来禁止处理器把上面的volatile读与下面的普通读重排序。LoadStore屏障用来禁止处理器把上面的volatile读与下面的普通写重排序。
上述volatile写和volatile读的内存屏障插入策略非常保守。在实际执行时,只要不改变volatile写-读的内存语义,编译器可以根据具体情况省略不必要的屏障。下面我们通过具体的示例代码来说明:
class VolatileBarrierExample {
int a;
volatile int v1 = 1;
volatile int v2 = 2;
void readAndWrite() {
int i = v1; //第一个volatile读
int j = v2; // 第二个volatile读
a = i + j; //普通写
v1 = i + 1; // 第一个volatile写
v2 = j * 2; //第二个 volatile写
}
… //其他方法
}
针对readAndWrite()方法,编译器在生成字节码时可以做如下的优化:
注意,最后的StoreLoad屏障不能省略。因为第二个volatile写之后,方法立即return。此时编译器可能无法准确断定后面是否会有volatile读或写,为了安全起见,编译器常常会在这里插入一个StoreLoad屏障。
上面的优化是针对任意处理器平台,由于不同的处理器有不同“松紧度”的处理器内存模型,内存屏障的插入还可以根据具体的处理器内存模型继续优化。以x86处理器为例,上图中除最后的StoreLoad屏障外,其它的屏障都会被省略。
前面保守策略下的volatile读和写,在 x86处理器平台可以优化成:
前文提到过,x86处理器仅会对写-读操作做重排序。X86不会对读-读,读-写和写-写操作做重排序,因此在x86处理器中会省略掉这三种操作类型对应的内存屏障。在x86中,JMM仅需在volatile写后面插入一个StoreLoad屏障即可正确实现volatile写-读的内存语义。这意味着在x86处理器中,volatile写的开销比volatile读的开销会大很多(因为执行StoreLoad屏障开销会比较大)。
二 Volatile的字节码
为了搞清楚内存屏障,我们扒开class字节码看一下,用javap -v -p class文件名(不要.class 后缀)运行
volatile int v1;
descriptor: I
flags: ACC_VOLATILE
.....
void readAndWrite();
descriptor: ()V
flags:
Code:
stack=3, locals=3, args_size=1
0: aload_0
1: getfield #52 // Field v1:I
4: istore_1
5: aload_0
6: getfield #54 // Field v2:I
9: istore_2
10: aload_0
11: iload_1
12: iload_2
13: iadd
14: putfield #72 // Field a:I
17: aload_0
18: iload_1
19: iconst_1
20: isub
21: putfield #52 // Field v1:I
24: aload_0
25: iload_2
26: iload_1
27: imul
28: putfield #54 // Field v2:I
31: return
除了其变量定义的时候有一个Volatile外,之后的字节码跟有无Volatile完全一样,于是我们又扒了下汇编代码
三 Volatile的汇编码
为了看到汇编码,要使用hsdis插件, 在mac系统下需要安装一个hsdis-amd64.dylib的插件。在网上找了一个,地址在这里。
下载下来后,将其放置到你的jre lib目录下即可。
mac系统上命令如下,
sudo mv ./hsdis-amd64.dylib /Library/Java/JavaVirtualMachines/jdk1.8.0_31.jdk/Contents/Home/jre/lib
然后再运行
java -XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly -Xcomp -XX:CompileCommand=dontinline,*VolatileBarrierExample.readAndWrite -XX:CompileCommand=compileonly,*VolatileBarrierExample.readAndWrite com.earnfish.VolatileBarrierExample > out.put
其中*VolatileBarrierExample.readAndWrite表示你运行的类.函数, com.earnfish.VolatileBarrierExample表示你的包名.类名,注意需要有main函数来运行你所要执行的函数。得出汇编码如下
0x000000011214bb49: mov %rdi,%rax
0x000000011214bb4c: dec %eax
0x000000011214bb4e: mov %eax,0x10(%rsi)
0x000000011214bb51: lock addl $0x0,(%rsp) ;*putfield v1
; - com.earnfish.VolatileBarrierExample::readAndWrite@21 (line 35)
0x000000011214bb56: imul %edi,%ebx
0x000000011214bb59: mov %ebx,0x14(%rsi)
0x000000011214bb5c: lock addl $0x0,(%rsp) ;*putfield v2
; - com.earnfish.VolatileBarrierExample::readAndWrite@28 (line 36)
其对应的Java代码如下
v1 = i - 1; // 第一个volatile写
v2 = j * i; // 第二个volatile写
可见其本质是通过一个lock指令来实现的。那么lock是什么意思呢?
查询IA32手册,它的作用是使得本CPU的Cache写入了内存,该写入动作也会引起别的CPU invalidate其Cache。所以通过这样一个空操作,可让前面volatile变量的修改对其他CPU立即可见。
所以,它的作用是
锁住主存
任何读必须在写完成之后再执行
使其它线程这个值的栈缓存失效
类似于前面是storestore,后面是storeload
java volatile内存屏障_从汇编看Volatile的内存屏障相关推荐
- java 内存分布_一图看懂JVM内存分布,永久记住!
经常在说JVM内存分布,也经常去看,但是总是在面试的时候说不清楚或者模糊,甚至有可能说错,只有真正的理解,并且在心中有一个总结构图才能记得清楚说的清楚! | JVM总览图 java内存区域主要分程序计 ...
- 从汇编看volatile与MESI的关系
JMM 1.来谈谈JMM 2.volatile和synchronized关键字 2.1volatile是如何保证可见性的?? 2.2 volatile是如何保证有序性的呢?? 哪些情况不能重排序?? ...
- java 堆内存结构_基于JDK1.8的JVM 内存结构【JVM篇三】
在我的上一篇文章别翻了,这篇文章绝对让你深刻理解java类的加载以及ClassLoader源码分析[JVM篇二]中,相信大家已经对java类加载机制有一个比较全面的理解了,那么类加载之后,字节码数据在 ...
- Linux内存管理:一个故事看懂CPU内存管理技术
目录 8086 32位时代 虚拟内存 分页交换 现在 往期热门回顾 推荐阅读 还记得我吗,我是阿Q,CPU一号车间的那个阿Q. 今天忙里偷闲,来到厂里地址翻译部门转转,负责这项工作的小黑正忙得满头大汗 ...
- mysql内存淘汰_从创建索引过程中内存变化来看SQL Server与MySQL的内存淘汰算法
在sqlserver中,几年之前就注意到一个现象:sqlserver中对一个大表创建索引或者rebuild索引的过程中,会引起内存剧烈的动荡,究其原因为何,这种现象到底正不正常,是不是sqlserve ...
- c++ thread 内存泄漏_深入剖析ThreadLocal原理、内存泄漏及应用场景
本文主要针对JDK1.8讲解 ThreadLocal作用 先看一个简单的示例,创建两个线程,第一个线程向ThreadLocal中写入数据,第二个线程等待第一个线程完成从ThreadLocal中读取数据 ...
- linux 内存密码_您的密码错误是内存问题
linux 内存密码 Let's play a game. Look at this string of characters for a minute and then see if you can ...
- python程序占用内存高_如何优化Python占用的内存,面试必学
如果程序处理的数据比较多.比较复杂,那么在程序运行的时候,会占用大量的内存,当内存占用到达一定的数值,程序就有可能被操作系统终止,特别是在限制程序所使用的内存大小的场景,更容易发生问题.下面我就给出几 ...
- kettle 内存设置_【转】kettle 的内存设置及输出日志的时间类型
本文转载自:http://blog.csdn.net/dqswuyundong/archive/2010/10/19/5952004.aspx 设置kettle的内存 REM ************ ...
最新文章
- 「Python」pycharm多项目虚拟环境切换
- 简单的通讯录程序系统python
- leetcode83,删除有序链表中的重复元素
- Python实现红黑树的插入操作
- sql服务器密码如何显示,如何查看sql数据库密码
- 按键精灵--VS挤房器_Fly_v2.5版
- web系统服务器登录不上去,宝塔面板严重错误登录不上怎么办
- android 仿微信demo————微信顶部操作栏搜索按钮实现(查询通讯录好友功能)
- 编程入门书籍:大学学习计算机基础必读 5 本经典入门书籍,收藏
- 印象最深刻的三位老师、难忘的往事
- python求解迷宫问题,配js实现的走迷宫动画,动起来才有意思~
- HRM人力资源系统-Day13
- 思途旅游CMS短信宝短信插件
- 主动降噪耳机哪个好?2021年双11主动降噪耳机推荐!
- 【js学习笔记三十九】简单工厂模式
- Android Java 必备:Socket通信
- 计算机在英语课堂的应用,浅谈信息化技术在英语课堂的应用
- 使用Stream处理Map
- COMSOL Multiphysics弱形式入门(一)
- 录屏软件哪个好?我推荐好用且免费的野葱