Java Core「3」volatile 关键字
当我们提到 volatile 关键字的作用时,想到的是可见性、原子性、禁止重排序。
01-可见性
可见性问题指一个线程修改了共享变量的值,而另外一个线程却看不到。造成这个问题的原因是线程中存在一个高速缓存区(working memory)。
我们从 Java Memory Model(JMM)和硬件角度分析下可见性出现的原因。
图 1.JMM 与 硬件架构之间的关系(右图来自于 jenkov.com)
线程中的 working memory 对应的是计算机硬件中的 CPU cache memory,用来解决内存与 CPU 之间访问速度的差异,提高运算速度。
在图 1.的基础上,我们举例说明为什么会出现可见性问题?假设主存中存在一个变量obj.count
,它的当前值为 1。
线程 A 在访问该变量时,会将其载入到自己的 working memory。随后,如果修改该变量的值,也并不会立刻写回到主存中(写回时机由操作系统控制)。
假设线程 A 将
obj.count
的值修改为 2,而 CPU cache memory 又恰巧尚未写回到主存,那么线程 B 此时从主存中读取的obj.count
的值就仍然是 1。
上面这个过程我们可以通过一个程序来验证下:
运行足够时间后,输出中会有如下的结果:
了解到可见性问题产生的原因,我们来看一下 volitale 是如何实现可见性的。volatile 指令实际上是通过 JVM lock
指令添加内存屏障实现可见性的[1]。
lock 前缀的指令在多核处理器下会引发两件事情:
将当前处理器缓存行的数据写回到系统内存。
写回内存的操作会使在其他 CPU 里缓存了该内存地址的数据无效。
02-原子性
volatile 是无法保证i++
操作的原子性的,因为i++
是一个复核操作,包含了:1)读取 i 的值;2)对 i 执行加 1 操作;3)将 i 的值写回内存。
但是对于 double 和 long 类型的变量,是鼓励使用 volatile 修饰的。因为 JLS 中解释:
Writes and reads of volatile long and double values are always atomic.
如果不使用 volatile ,在图 1.中的主存与 working memory 之间的 read/write 和 load/store 操作都是将其当作是两个对立的 32 位变量来对待。
不过,现在 JVM 普遍都将 64 位数据的读写当作是原子操作。一般情况下,不使用 volatile 修饰 long 或 double 变量也不会出错。
03-有序性
JLS 中关于 volatile 变量有一条 happens-before 规则:
A write to a volatile field happens-before every subsequent read of that field.
从前面的章节中了解到,volatile 实现可见性是通过插入内存屏障。内存屏障还有一个其他的作用就是,禁止指令重排序。
04-volatile 的应用场景
在单例模式中,单例对象一般会被 volatile 修饰。例如:
这里使用 volatile 的主要原因是防止指令重排序。因为,singleton = new Singleton();
是一个复合操作,它包括:
分配内存空间
初始化对象
将对象地址的引用赋值给
singleton
禁止指令重排序是为了避免对象在初始化之前被返回。
历史文章
版权声明: 本文为 InfoQ 作者【Samson】的原创文章。
原文链接:【http://xie.infoq.cn/article/cc8c8371bbf409f347ed4cfa0】。文章转载请联系作者。
评论