用法
volatile
是 Java 中的关键字,直接修饰成员变量,不能和 final
关键字同时使用。
private volatile boolean flag = false;
作用
当一个变量被声明为volatile时,它可以确保以下两点:
保证可见性:当一个线程修改了一个volatile变量的值,其他线程会立即看到这个变动。这是因为volatile关键字会禁止CPU缓存和编译器优化,从而确保每次读取变量时都会直接从主内存中获取最新值,而不是从本地缓存中读取。这样可以确保在多线程环境下变量值的实时同步。
保证有序性:volatile关键字还能确保指令的执行顺序不被重排。Java内存模型允许编译器和处理器对指令进行重排序,以提高执行效率。但是,在某些情况下,这种重排序可能导致多线程环境下的数据不一致。volatile关键字可以防止这种情况发生,确保指令执行的顺序符合程序员的预期。
保证内存可见原理
早期计算机中cpu和内存的速度是差不多的,但在现代计算机中,CPU的指令速度远超内存的存取速度,由于计算机的存储设备与处理器的运算速度有几个数量级的差距,所以现代计算机系统都不得不加入一层读写速度尽可能接近处理器运算速度的高速缓存(Cache)来作为内存与处理器之间的缓冲。
将运算需要使用到的数据复制到缓存中,让运算能快速进行,当运算结束后再从缓存同步回内存之中,这样处理器就无须等待缓慢的内存读写了。
基于高速缓存的存储交互很好地解决了处理器与内存的速度矛盾,但是也为计算机系统带来更高的复杂度,因为它引入了一个新的问题:缓存一致性(CacheCoherence)。
为了解决一致性的问题,需要各个处理器访问缓存时都遵循一些协议,在读写时要根据协议来进行操作,这类协议有MSI
、MESI
、MOSI
、Synapse
、Firefly
及DragonProtocol
等。
这里我们关于Intel的MESI说明
MESI(缓存一致性协议)
当CPU写数据时,如果发现操作的变量是共享变量,即在其他CPU中也存在该变量的副本,会发出信号通知其他CPU将该变量的缓存行置为无效状态,因此当其他CPU需要读取这个变量时,发现自己缓存中缓存该变量的缓存行是无效的,那么它就会从内存重新读取。
至于是怎么发现数据是否失效呢?
总线嗅探机制
每个处理器通过嗅探在总线上传播的数据来检查自己缓存的值是不是过期了,当处理器发现自己缓存行对应的内存地址被修改,就会将当前处理器的缓存行设置成无效状态,当处理器对这个数据进行修改操作的时候,会重新从系统内存中把数据读到处理器缓存里。
所以当变量被volatile修饰时,当共享变量被线程1修改后,立即写回主内存,同时经过总线,线程2通过总线嗅探机制,将自己工作内存中变量设置为无效状态,当线程2需要读取这个变量时,发现自己缓存中缓存该变量的缓存行是无效的,那么它就会从内存重新读取。
对于共享变量,每个线程操作数据的时候会把数据从主内存读取到自己的工作内存,如果他操作了数据并且写回了,他其他已经读取的线程的变量副本就会失效了,需要都数据进行操作又要再次去主内存中读取了。
但是Volatile也会导致一些问题,例如总线风暴
由于使用volatile
和MESI缓存一致性协议,需要不断的从主内存嗅探和cas不断循环,无效交互会导致总线带宽达到峰值。
所以不要大量使用Volatile,至于什么时候去使用Volatile什么时候使用锁,根据场景区分。
禁止指令重排序原理
什么是指令重排序?
为了提高性能,编译器和处理器常常会对既定的代码执行顺序进行指令重排序。
重排序的类型有哪些呢?源码到最终执行会经过哪些重排序呢?
那Volatile是怎么保证不会被执行重排序的呢?
内存屏障
java编译器会在生成指令系列时在适当的位置会插入内存屏障指令来禁止特定类型的处理器重排序。
volatile 修饰的变量写操作是在前面和后面分别插入内存屏障,而读操作是在后面插入两个内存屏障。
参考 https://blog.csdn.net/qq_38905818/article/details/105831893