「 天行健,君子以自强不息。地势坤,君子以厚德载物。」———《易经》
volatile 变量,在上一篇文章中已经有简单提及相关概念和用法,这一篇主要对 Volatile 变量的特性进行源码验证。验证它的涉及到的三个特性:
- 可见性
- 指令重排序
- 非原子性
volatile 之可见性验证
上一篇文章中,讲到 volatile 变量通常被当做状态标记使用。其中典型的应用是,检查标记状态,以确定是否退出循环。下面我们直接举个反例,源码如下:
1 | public class Volatile { |
上面代码,语句 A 一定在语句 B 之前执行吗? 答案是否定的。因为这里可能发生指令重排序。语句 B 可能先于语句 A 先自行。
什么是指令重排序?处理器为了提高运行效率,可能对输入代码进行优化,他不保证程序中各个语句的执行先后顺序同代码中的顺序一致,但是他会保证程序最终执行的结果和代码顺序执行的结果是一致的。
但是下面这种情况,语句 B 一定在 语句 A 之后执行。
1 | int a = 1; |
原因是,变量 b 依赖 a 的值,重排序时处理器会考虑指令之间的依赖性。
当然,这个 volatile 有什么关系呢?
volatile 变量可以一定程度上保证有序性,volatile 关键字禁止指令重排序。
1 | //x、y为非volatile变量 |
这里要说明的是,flag 为 volatile 变量;能保证
- 语句1,语句2 一定是在语句3的前面执行,但不保证语句1,语句2的执行顺序。
- 语句4,语句5 一定是在语句3的后面执行,但不保证语句4,语句5的执行顺序。
- 语句1,语句2 的执行结果,对语句3,语句4,语句5是可见的。
以上,就是关于 volatile 的禁止重排序的说明。、
volatile 之非原子性问题验证
volatile 关键字并不能保证原子性,如自增操作。下面看一个例子:
1 | public class Volatile{ |
这个程序执行的结果并没有达到我们的期望值,1000。并且每次的运行结果可能都不一样,如下图,有可能是 997 等。
来看下面一副图,分解自增操作的步骤。
- read&load 从主内存复制变量到当前工作内存。
- use&assign 执行代码,改变共享变量的值。
- store&write 用工作内存数据刷新主内存相关内容。
但是,这一系列的操作并不是原子的。也就是在 read&load 之后,如果主内存 count 发生变化,线程工作内存中的值由于已经加载,不会产生对应的变化。所以计算出来的结果和我们预期不一样。
对于 volatile 修饰的变量,jvm 虚拟机只是保证从主内存加载到线程工作内存中的值是最新的。
所以,假如线程 A 和 B 在read&load 过程中,发现主内存中的值都是5,那么都会加载这个最新的值 5。线程 A 修改后写到主内存,更新主内存的值为6。线程 B 由于已经 read & load,注意到此时线程 B 工作内存中的值还是5, 所以修改后也会将6更新到主内存。
那么两个线程分别进行一次自增操作后,count 只增加了1,结果也就错了。
当然,我们可以通过并发安全类AomicInteger, 内置锁 sychronized,显示锁 ReentrantLock,来规避这个问题,让程序运行结果达到我们的期望值 1000.
1)采用并发安全类 AomicInteger 的方式:
1 | import java.util.concurrent.atomic.AtomicInteger; |
2) 采用内置锁 synchronized 的方式:
1 | public class Volatile{ |
3)采用显示锁的方式
1 | import java.util.concurrent.locks.Lock; |
参考
http://www.cnblogs.com/dolphin0520/p/3920373.html
https://blog.csdn.net/gao_chun/article/details/45095995
https://blog.csdn.net/xilove102/article/details/52437581
本文原创首发于微信公众号 [ 林里少年 ],欢迎关注第一时间获取更新。