0%

Java知识点:volatile关键字

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 编译器,使其不执行该优化。

参考

漫画:什么是volatile关键字?(整合版)

ifeve - Java Volatile关键字