当一个变量定义为volatile之后,它具备两种特性:
- 保证此变量对所有线程的可见性,这里的“可见性”是指当一条线程修改了这个变量的值,新值对于其他线程来说是可以立即得知的。
- 禁止指令重排序优化。
在X86处理器下通过工具获取 JIT编译器生成的汇编指令来看下volatile变量进行读写操作时CPU的行为:
Java 代码如下:
// volatile Object instance;
instance = new Singleton();
生成的汇编代码如下:
0x01a3de1d: movb $0X0, 0X1104800(%esi); 0X01a3de24: lock addl $0X0, (%esp);
有volatile变量修饰的共享变量进行写操作的时候会多出第二行汇编代码,Lock前缀的指令在多核处理器下会引发了两件事件。
1. 将当前处理器缓存的数据写回到系统内存。
2. 这个写回内存的操作会使在其他CPU里缓存了该内存地址的数据无效。
再让我们从Java内存模型的角度分析下volatile变量。假定T表示一个线程,V和W分别表示两个volatile变量,那么在进行read, load, use, assign, store和write时需要满足以下三条规则:
只有当线程T对变量V执行的前一个动作是load时,T才能对V执行use; 并且,只有当T对V执行的后一个动作是use时,T才能对V执行load。T对V的use动作可以认为是和线程T对V的load,read动作相关联,必须连续一起出现(这条规则要求 在工作内存中,每次使用V前都必须先从主内存刷新最新的值,用于保证能看见其他线程对变量V所做的修改后的值)。
只有当线程T对变量V执行的前一个动作是assign时,T才能对V执行store动作;并且,只有当T对变量V执行的后一个动作是store时,线程T才能对变量V执行assign动作。线程T对变量V的assign动作可认为是和线程T对变量V的store, write动作相关联,必须连续一起出现(这条规则要求 在工作内存中,每次修改V后都必须立刻同步回主内存中,用于保证其他线程可以看到自己对变量V所做的修改)。
假定动作A是线程T对变量V实施的use或assign操作,假定动作F是和动作A相关联的load或store动作,假定动作P是和动作F相应的变量V的read或write动作;类似的,假定动作B是线程T对变量W实施的use或assign动作,假定动作G是和动作B相关联的load或store动作,假定动作Q是和动作G相应的变量W的read或write动作。如果A先于B,那P先于Q(这条规则要求 volatile修饰的变量不会被指令重排序优化,保证代码的执行顺序与程序的顺序相同)。
由于volatile变量只能保证可见性,在不符合以下两条规则的运算场景中,仍然要通过加锁(使用synchronized或java.util.concurrent中的原子类)来保证原子性:
运行结果并不依赖变量的当前值,或者能够确保只有单一的线程修改变量的值。
变量不需要与其他的状态变量共同参与不变约束。
在某些情况下,volatile的同步机制性要优于锁。并且,volatile变量读操作的性能消耗与普通变量几乎没有什么差别,但是写操作则可能会慢一些,因为它需要在本地代码中插入许多内存屏障指令来保证处理器不发生乱序执行。