本讲座地址https://segmentfault.com/l/15... 欢迎大家围观
Java的Volatile的特征是任何读都能读到最新值,本质上是JVM通过内存屏障来实现的,让我们看看从字节码以及汇编码的角度,来看下是否真是如此?
一 Volatile与内存屏障
本节内容来自:http://www.infoq.com/cn/artic...
为了实现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
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。