volatile
关键字的作用主要是:
- 保证变量可见性
- 限制指令重排
volatile
关键字涉及的知识点可谓是比较多的。
变量可见性
首先,变量可见性与 Java 内存模型(JMM, Java Memory Model)有关。
对于一个共享变量(比如静态变量),它保存在所有线程共享的主内存中,而每个线程的工作内存都有一个该变量的“副本”,线程读写变量就涉及到两个内存中值的传递。
这里所说的线程的工作内存,即 CPU 缓存。
应该注意的是,对于多核 CPU 而言,不同的线程可能运行在不同的核中,并使用不同的缓存。
现在,我们先用 A 线程修改共享变量,再用 B 线程读取变量值。
根据 happens-before 原则,A 线程修改在前,B 线程读取在后,所以 B 线程读取到的应是 A 线程修改后的变量。
但事实是,B 线程可能在 A 线程修改工作内存而还没有刷回主内存时读取到旧的变量值。
而 volatile
变量会保证工作内存和主内存的同步,而不会出现上述问题。
要注意的是,volatile
只保证变量可见性,但并非线程安全的,也没有原子性。
volatile的使用限制
使用时通常有以下一些限制:
- 运行结果不依赖变量当前值。(i++ 这种操作就不能有)
- 不要与其他状态变量共同参与不变约束。(两个变量关系作为循环条件也不行)
- 一写多读。(只有单一线程修改变量)
volatile变量的读写“副作用”
值得注意的是,volatile
变量在读写的时候不仅仅影响自身,还会带来一些“副作用”:
如果一线程读取了一个
volatile
变量,那么该线程中所有可见变量都会从主存重新读取;如果一线程定稿了一个
volatile
变量,那么该线程中所有可见变量也会写回主存。
限制指令重排
另外,volatile
还限制指令重排。
指令重排即对执行的指令进行重新排序,目的是效率优化。但指令重排只保证单线程下结果不变,多线程下可能影响执行结果。volatile
引入 Memory Barrier(内存屏障/内存栅栏/栅栏指令)约束了重排,以确保结果的正确性。
总之,volatile
如其字面含义所指的一样,表示修饰的变量是易变的,因此内部将通过一系列手段确保总是使用其最新值。
编译器优化问题
JIT 编译器会将一个线程没有修改到的非 volatile
类型的字段进行内联,一旦该代码被编译(可通过 -XX:+PrintCompilation 查看),另一个线程对这个字段的修改可能永远也观察不到。
加上随机的同步块或打印语句可以推迟这个优化的执行,或扰乱 JIT 编译器,使其不执行该优化。