通常,为了确保共享变量一致且可靠地更新,线程应该通过获取锁来确保它独占使用这些变量,该锁通常对这些共享变量强制互斥。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++
的指令重排序。
Reference
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