volatile作用
保证不同线程之间对共享变量操作时的可见性
在多线程的环境下,如果某个线程首次读取共享变量,则首先到主内存中获取该变量,然后存入工作内存中,以后只需要在工作内存中读取该变量即可。同样如果对该变量执行了修改操作,则先将新值写入工作内存中,然后再刷新至主内存中。但是什么时候最新的至会被刷新至主内存是不确定的,所以会导致线程间的共享变量不可见。
当一个变量被volatile关键字修饰时,对于共享资源的读操作会直接在主内存中进行,当然也会缓存到工作内存中,当其他线程对该共享资源进行了修改,则会导致当前线程在工作内存中的共享资源失效(反映到硬件上就是CPU的L1或L2的Cache Line失效,MESI协议[1]),所以必须从主内存中再次获取。对于共享资源的写操作是要先修改工作内存,但是修改结束后会立刻将其刷新到主内存中。
禁止对指令进行重排序操作
在Java的内存模型中,允许编译器和处理器对指令进行重排序(目的是提高CPU效率),在单线程的情况下,重排序不会引起什么问题,但是在多线程的情况下,重排序会影响到程序的正确运行。
volatile关键字修饰的变量在C++中会加"lock"指令,"lock"实际上内存屏障[2]
volatile在JVM中的实现:
StoreStoreBarrier ┃ volatile写操作 ┃ StoreLoadBarrier
LoadLoadBarrier ┃ volatile读操作 ┃ LoadStoreBarrier
[1] MESI协议
MESI协议保证了每一个缓存中使用的共享变量副本都是一致的,它的大体思想是,当CPU在操作Cache中的数据时,如果发现该变量是一个共享变量,也就是说在其他的CPU中也存在一个副本,那么进行如下的操作:
-
读取操作,不作任何处理,只是将Cache中的数据读取到寄存器。
-
写入操作,发出信号通知其他CPU将该变量的Cache Line置为无效状态,其他CPU在进行该变量读取的时候不得不到主内存中再次获取。
[2] 内存屏障
cpu硬件级别的内存屏障
sfence: 在sfence指令前的写操作当必须在sfence指令后的写操作前完成。
lfence:在lfence指令前的读操作当必须在lfence指令后的读操作前完成。
mfence:mfence指令前的读写操作当必须在mfence指令后的读写操作前完成。(全能屏障)
jvm规范中的内存屏障
LoadLoad屏障:读读屏障(load1|loadload|load2)(load1必须在load2之前运行读取完以下三个类似)
StoreStore屏障:写写屏障(store1|storestore|store2)
LoadStore屏障:读写屏障(load|loadstore|store)
StoreLoad平常:写读屏障(store|storeload|load)(全能屏障)