当一个变量被volatile
修饰之后,可以保证此变量对所有线程的可见性,这里的“可见性”是指当一条线程修改了这个变量的值,新值对于其它线程来说是可以立即得知的。而普通变量并不能做到这一点,普通变量的值在线程间传递时均需通过主内存来完成。
volatile
的保证线程可见性的底层是通过CPU的缓存一致性协议来完成的,这里只是简单介绍下缓存一致性协议,不去深追CPU里面的各种原语。
MESI(Modified Exclusive Shared Or Invalid)(也称为伊利诺斯协议,是因为该协议由伊利诺斯州立大学提出)是一种广泛使用的支持写回策略的缓存一致性协议。
CPU
中每个缓存行cache line(64bytes)使用4种状态进行标记(使用额外的两位(bit
)表示):
该缓存行只被缓存在该
CPU
的缓存中,并且是被修改过的,即与主存中的数据不一致。该缓存行中的内存需要在未来的某个时间点(允许其它
CPU
读取主存中相应内存之前)写回主存。当被写回主存之后,该缓存行的状态会变成Exclusive状态。
该缓存行只被缓存在该
CPU
的缓存中,它是未被修改过的,与主存中数据一致。该状态可以在任何时刻当有其它
CPU
读取该内存时变成Shared状态。同样地,当
CPU
修改该缓存行中内容时,该状态可以变成Modified状态。
该状态意味着该缓存行可能被多个
CPU
缓存,并且各个缓存中的数据与主存数据一致。当有一个
CPU
修改该缓存行中的数据时,其它CPU
中该缓存被变成Invalid状态。
该缓存是无效的(可能有其它
CPU
修改了该缓存行)。
- 字节码层面 ACC_VOLATILE
- JVM层面,底层用了4个内存屏障来实现禁止指令重排序。
StoreStoreBarrier
volatile 写操作
StoreLoadBarrier
LoadLoadBarrier
volatile 读操作
LoadStoreBarrier
实例请参考单例设计模式中的DCL(Double Check Lock)
。
一个变量
i=100
被volatile
修饰,两个线程想对这个变量修改,都对其进行自增操作也就是i++
,i++
的过程可以分为三步,首先获取i
的值,其次对i
的值进行加1,最后将得到的新值刷新到主存中。首先线程
A
读取了i
的变量的值,这个时候线程切换到了B
,线程B
同样从主内存中读取i
的值,由于线程A
没有对i
做过任何修改,此时线程B
获取到的i
仍然是100
。线程B
工作内存中为i
执行了加1的操作,但是没有刷新到主内存中,这个时候又切换到了A
线程,A
线程直接对工作内存中的100进行加1操作(因为A
线程已经读取过i
的值了),由于线程B
并未写入i
的最新值,这个时候A
线程的工内存中的100不会失效。 最后,线程A
将i=101
写入主内存中,线程B
也将i=101
写入主内存中。 始终需要记住,i++
的操作是3步骤!
volatile最适用一个线程写,多个线程读的场合。
如果有多个线程并发写操作,仍然需要使用锁或者线程安全的容器或者原子变量来代替。(摘自Netty权威指南)
- 《深入理解Java虚拟机第3版》
- 《Java并发编程的艺术》