通常,为了确保共享变量一致且可靠地更新,线程应该通过获取锁来确保它独占使用这些变量,该锁通常对这些共享变量强制互斥。Java 编程语言提供了第二种机制,即 volatile 字段,在某些场景下,它比使用锁更方便。一个字段可以被声明为 volatile,在这种情况下,Java 内存模型确保所有线程看到变量的一致值。
比如下面的例子,一个线程不停调用方法 one,另一个线程不停调用方法 two:
1 | package me.tianshuang; |
咋眼一看可能以为输出结果只可能为 j 大于等于 i,因为方法 two 获取两个变量中间的时隙可能会有方法 one 执行,但是实际上因为以上代码没有引入同步机制,且因为指令重排序,可能导致在 one 所处的线程中 j++ 先于 i++ 执行。在我的电脑上,产生了如下的输出:
1 | i=1276138785 j=1276138793 |
通过以上输出可以观察到,打印出的 i 可能等于 j,可能大于 j,也可能小于 j。上面这个例子来自于 Example 8.3.1.4-1. volatile Fields,个人认为这个例子并不是很好,因为同时涉及到了以下问题:
- 方法
one执行时线程内部的指令重排序 - 方法
two执行时获取两个变量中间时隙可能方法one被调度执行 - 即使变量
i和j在执行方法one时被更新,调用方法two的线程因为线程本地缓存也可能看不到最新的变量值
关于这个例子的讨论可见 how to understand volatile example in Java Language Specification? - Stack Overflow,感兴趣的可以看看。
可以在两个方法上使用 synchronized 修饰符来规避上面这个例子中的乱序行为,如以下代码:
1 | package me.tianshuang; |
在两个方法上都使用了 synchronized 修饰符后,有了以下的保证:
- 方法
one及方法two只能串行执行,比如方法two在执行过程中方法one不可能被调度执行 - 方法
two能够看到变量i及变量j的最新值
因为有了以上保证,i 与 j 的输出值将完全一致。
另一种方式就是使用 volatile 修饰符修饰变量 i 及变量 j,如以下代码:
1 | package me.tianshuang; |
使用 volatile 修饰符修饰变量 i 和变量 j 后,有了以下保证:
- 禁止了方法
one中的两个变量执行的指令重排序 - 保证了方法
two获取两个变量时能够看到该变量的最新的值
有了以上的保证,那么方法 one 中的 j++ 不可能先于 i++ 执行,理论上不可能出现变量 j 的值大于变量 i 的值的情况,但是我们在本地执行程序,将会观察到打印出来的值中出现了 j 大于 i,比如如下输出:
1 | i=305491464 j=305491464 |
这个很好解释,是因为方法 two 获取变量 i 及变量 j 中间的时隙方法 one 被调度执行了,所以输出值中会有 j 大于 i 的数据,如果仔细观察所有的输出值,不会有 i 大于 j 的数据,这是因为 volatile 禁止掉了方法 one 执行时语句 i++ 与语句 j++ 的指令重排序。
References
8.3.1.4. volatile Fields
is volatile required with synchronized? - Stack Overflow
Is the volatile keyword required for fields accessed via a ReentrantLock? - Stack Overflow
Volatile: why prevent compiler reorder code - Stack Overflow
volatile (computer programming) - Wikipedia
Is volatile expensive? - Stack Overflow