volatile的通俗含义是共享变量的一致性,即在一个线程更改了该共享变量的值的时候,其他的线程能立马知道改动的发生,并且获取到新值,这一性质也可以称为“可见性”。
它在使用恰当时,开销将比synchronized更低,因为volatile不会引起线程上下文切换和调度。在某些情况下,使用volatile将比synchronized简单且效率更高。
volatile的底层实现
要理解volatile的底层实现,首先要知道cpu内部的缓存工作原理。对于cpu内部的多级缓存工作原理,可以去详细看看这篇博客:CPU缓存。此处不再赘述。
对于被volatile修饰的变量,编译后的汇编代码会多出一个带有lock前缀的指令。该指令会使得cpu在运行指令时,做出以下两个操作:
1.将当前处理器缓存行的数据写回至系统内存
2.使cpu其他缓存了该内存地址的数据无效化
接下来将详细解释这两步操作。
1.将当前缓存行的数据写回至系统内存
Lock前缀指令导致在执行指令期间,声言处理器的LOCK#信号。Lock#信号会确保在声言该信号期间独享cpu内存(有两种方式确保,在以前的处理器往往是锁住系统总线,从而保证cpu无法访问系统内存,最近的cpu对该信号做出了优化,将锁总线替换为了封锁缓存,从而保证自己独占cpu的缓存)。针对不同型号的cpu,对LOCK#信号的处理也不相同。总之,在声言LOCK#时,cpu的缓存将被独占,并将该区域的内容写回至内存,并使用缓存一致性机制来确保修改的原子性。此操作被称为“缓存锁定”,缓存一致性机制会阻止同时修改由两个以上处理器缓存的内存区域数据。
2.使cpu其他缓存了该内存地址的数据无效化
IA-32处理器及Intel64处理器使用嗅探技术保证它的内部缓存,系统内存和其他处理器的缓存的数据在总线上保持一致。在Pentium处理器中,通过嗅探一个处理器来检测其他处理器打算写内存地址,而地址处于共享状态,那么嗅探的处理器使它的缓存行无效,并在下次访问相同地址时,强制执行缓存行充填。
注意这里的整行填充。对于volatile修饰的变量,有一种追加字节的优化方式。很多处理器的缓存行大小为64字节,对于一个volatile的变量引用,占用了四个字节,此时,一个缓存行中除了该变量,还存在其他的有用数据,在读写一次该变量后,处理器强行刷新该引用所在的缓存行,导致其他有用的数据可能会导致cache miss,降低运行效率。追加字节的方式便是对于一个volatile的引用类追加字节,使它达到64字节,刚好占据一整行缓存行,降低了cache miss的概率。
对于非64字节为一个缓存行大小的处理器,以及不会被频繁写入的共享变量,这样的优化便不适用了。
参考资料
《java并发编程的艺术》
CPU缓存
40450418