java编程,如何彻底理解volatile关键字?( 三 )


Java 编译器在生成指令序列的适当位置会插入内存屏障来禁止特定类型的处理去重排序 。从而让程序按我们预想的流程去执行 。
① 保证特定操作的执行顺序
② 影响某些数据(或者是某条指令的执行结果)的内存可见性
编译器和CPU能够重排序指令 。保证最终相同的结果 。尝试优化性能 。插入一条Memory Barrier 会告诉编译器和CPU。不管什么指令都不能和这条Memory Barrier 指令重排序 。
Memory Barrier 所做的另外一件事是强制刷出各种CPU cache, 如一个Write-Barrier(写入屏障)将刷出所在的Barrier 之前写入cache的数据 。因此 。任何CPU上的线程都能读取到这些数据的最新版本 。
JMM把内存屏障指令分为4类:

java编程,如何彻底理解volatile关键字?

文章插图
StoreLoad Barriers 是一个\"全能型\"的屏障 。它同时具有其他3个屏障的效果 。
volatile 关键字介绍
1、保证可见性
对一个volatile变量的读 。总是能看到(任意线程)对这个volatile变量最后的写 。
我们先看下面代码:
java编程,如何彻底理解volatile关键字?

文章插图
initFlag 没有用volatile关键字修饰;
上面结果为:
java编程,如何彻底理解volatile关键字?

文章插图
说明一个线程改变initFlag状态 。另外一个线程看不见;
如果加上volatile关键字呢?
结果如下:
java编程,如何彻底理解volatile关键字?

文章插图
我们通过汇编看下代码的最终底层实现:
java编程,如何彻底理解volatile关键字?

文章插图
volatile写的内存语义如下:
当写一个volatile变量时 。JMM会把该线程对应的本地内存中的共享变量值刷新到主内存 。
当读一个volatile变量时 。JMM会把该线程对应的本地内存置为无效 。线程接下来将从主内存中读取共享变量 。
比如:
java编程,如何彻底理解volatile关键字?

文章插图
如果我们将flag变量以volatile关键字修饰 。那么实际上:线程A在写flag变量后 。本地内存A中被线程A更新过的两个共享变量的值都被刷新到主内存中 。
java编程,如何彻底理解volatile关键字?

文章插图
在读flag变量后 。本地内存B包含的值已经被置为无效 。此时 。线程B必须从主内存中读取共享变量 。线程B的读取操作将导致本地内存B与主内存中的共享变量的值变成一致 。
java编程,如何彻底理解volatile关键字?

文章插图
如果我们把volatile写和volatile读两个步骤综合起来看的话 。在读线程B读一个volatile变量后 。写线程A在写这个volatile变量之前所有可见的共享变量的值都将立即变得对读线程B可见 。
2、原子性
volatile 不保证变量的原子性;
java编程,如何彻底理解volatile关键字?

文章插图
运行结果如下:
java编程,如何彻底理解volatile关键字?

文章插图
因为count ++;
包含 三个操作:
(1) 读取变量count
(2) 将count变量的值加1
(3) 将计算后的值再赋给变量count
从JMM内存分析:
java编程,如何彻底理解volatile关键字?

文章插图
下面从字节码分析为什么i++这种的用volatile修改不能保证原子性?
javap : 字节码查看
java编程,如何彻底理解volatile关键字?

文章插图
其实i++这种操作主要可以分为3步:(汇编)
读取volatile变量值到local
增加变量的值
把local的值写回 。让其它的线程可见
java编程,如何彻底理解volatile关键字?

文章插图
Load到store到内存屏障 。一共4步 。其中最后一步jvm让这个最新的变量的值在所有线程可见 。也就是最后一步让所有的CPU内核都获得了最新的值 。但中间的几步(从Load到Store)是不安全的 。中间如果其他的CPU修改了值将会丢失 。
3、有序性
(1) volatile重排序规则表
java编程,如何彻底理解volatile关键字?

文章插图
① 当第二个操作是volatile写时 。不管第一个操作是什么 。都不能重排序 。这个规则确保volatile写之前的操作不会被编译器重排序到volatile写之后 。
② 当第一个操作是volatile读时 。不管第二个操作是什么 。都不能重排序 。这个规则确保volatile读之后的操作不会被编译器重排序到volatile读之前 。

推荐阅读