一. 前言
volatile在JAVA和C/C++中均有使用,而含义不同。不论是前者还是后者,在网上都流传着诸多误解,为此我们抛开一切中文资料,从JAVA、C/C++的文档手册、开发者的博客、stack overflow的高赞问题回复等源头去着手了解,并结合实际代码测试来验证其说法的可靠性。
二. Cache和内存
众所周知,CPU有着高速缓存,而程序执行的时候,首先触发缺页中断加载如物理内存,然后由物理内存映射至CPU缓存,最后予以执行。而对于值保持不变的,我们会直接从缓存中读取,而不需要再去内存中重新读取,从而加快访问速度。而volatile,则是告诉编译器,该变量最好还是去内存读取,因为可能会被外部改变。 但是同样的语义,因为JVM和汇编的区别导致了其实现效果存在差异。
三. JAVA的volatile
首先来看一个来自于DZone社区的例子
1 | public class VolatileTest { |
其输出如下,不难理解。每次MY_INT
的改变会被ChangeListener ()
监听并输出。
1 | Incrementing MY_INT to 1 |
而如果我们去掉了volatile
,则输出会变成了
1 | Incrementing MY_INT to 1 |
造成该问题的核心点在于,volatile会告诉JVM,该变量可能会被别的线程修改,因此它会确保我们对于这个变量的读取和写入,都一定会同步到内存里,而不是从CPU的 Cache 里面读取。如果不加该关键字,则JVM可能会对程序进行一些优化,如从Cache中获取值(尤其是在看起来不会发生变化时),这就导致该线程会一直在死循环中忙忙碌碌。
四. C/C++的volatile
同样的我们先来看一段代码
1 | // global shared data |
这段代码将 thread1
作为主线程,等待 thread2
准备好 value
。因此,thread2
在更新 value
之后将flag
置为真,而 thread1
死循环地检测 flag
。但是,这段代码是有问题的。
- 在
thread1
中,flag = false
赋值之后,在while
死循环里,没有任何机会修改flag
的值,因此在运行之前,编译器优化可能会将if (flag == true)
的内容全部优化掉 - 在
thread2
中,尽管逻辑上update()
需要发生在flag = true
之前,但编译器和 CPU 并不知道;因此编译器优化和 CPU 乱序执行可能会使flag = true
发生在update()
完成之前,因此thread1
执行apply(value)
时可能value
还未准备好。
加上volatile
是否可行呢?如果只对flag
加上关键词是不够的,虽然if
判断不会被优化掉了,但是编译器仍有可能在优化时将thread2
中的 update
和对 flag
的赋值交换顺序。
如果给value
也加上呢?理论上来说是可以的,因为x86 和 AMD64 架构的 CPU(大多数个人机器和服务器使用这两种架构的 CPU)只允许 store-load 乱序,而不会发生 store-store 乱序。但是对于其他架构则无法保证,因此依然存在乱序执行的可能。
通过该例子,我们学到了两件事
- C/C++的volatile用于告诉编译器不要对该变量进行优化,从而避免了一些编译器优化导致的隐性问题。
- C/C++直接面向汇编,因此在多线程中volatile不像JAVA的volatile有JVM兜底,可以保证多线程的准确性。因此不建议在多线程中采用。
总结
对于JAVA来说,volatile告诉了编译器每次都从内存中读取数值以保证其准确性,可以适当的使用。对于C/C++来说,多线程有太多的更好的选择,volatile的使用场景应局限于嵌入式系统或设备驱动中,需要读/写内存映射硬件设备时,告诉编译器不要将该部分进行了错误优化。
参考文献
[1] Java Volatile Keyword Explained by Example
[2] Why the “volatile” type class should not be used
[3] Should volatile Acquire Atomicity and Thread Visibility Semantics?
[4] What’s the difference of the usage of volatile between C/C++ and C#/Java?