java语言规范第三版中有这样一句话:java编程语言允许线程访问共享变量,为了确保共享变量能被准确和一致的更新,线程应确保通过排他锁单独获得这个变量。
java语言提供了volatile,如果一个字段被声明为volatile,java线程内存模型将确保所有线程看到这个变量的值是一致的。在某些情况下比锁更加方便,因为它比synchronized的使用和执行成本更低,不会引起线程上下文的切换和调度,所以它也可以被看作轻量级的synchronized。
了解volatile实现原理之前,先需要了解相关的CPU术语与说明
内存屏障:是一组处理器指令,用于实现对内存操作的顺序限制(看volatile知识的时候总涉及这个)
有volatile变量修饰的共享变量进行写操作的时候会比普通的多出一行含有Lock前缀指令修饰的汇编代码
如下图:
Lock前缀指令实际上相当于一个内存屏障(也成内存栅栏),内存屏障会提供3个功能:
(1)它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成。
(2)将当前处理器缓存行的数据写回到系统内存。
(3)如果是写操作,它会导致其他CPU中对应的缓存行无效。
为了提高处理速度,处理器不直接和内存进行通信,而是先将系统内存的数据读到内部缓存(L1,L2或其他)后再进行操作,但操作完不知道何时会写到内存。如果对声明了volatile的 变量进行写操作,JVM就会向处理器发送一条Lock前缀的指令,将这个变量所在缓存行的数据写回到系统内存。
那么问题来了,就算写回到内存,那其他处理器缓存的值还是旧的,如果用旧值去计算操作仍会有问题(面试问题)
所以此时就需要用到缓存一致性协议来保证各个处理器的缓存是一致的,每个处理器通过嗅探在总线上传播的数据来检查自己缓存的值是不是过期了,当处理器发现自己缓存行对应的内存地址被修改了,就会将当前处理器的缓存行设置为无效状态,当处理器对这个数据进行修改操作的时候,会重新从系统内存中把数据读到处理器缓存中。
Lock前缀指令实际上相当于一个内存屏障(也称内存栅栏),它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成。
屏障类型指令示例说明LoadLoad BarriersLoad1;LoadLoad;Load2该屏障确保Load1数据的装载先于Load2及其后所有装载指令的的操作StoreStore BarriersStore1;StoreStore;Store2该屏障确保Store1立刻刷新数据到内存(使其对其他处理器可见)的操作先于Store2及其后所有存储指令的操作LoadStore BarriersLoad1;LoadStore;Store2确保Load1的数据装载先于Store2及其后所有的存储指令刷新数据到内存的操作StoreLoad BarriersStore1;StoreLoad;Load2该屏障确保Store1立刻刷新数据到内存的操作先于Load2及其后所有装载装载指令的操作。它会使该屏障之前的所有内存访问指令(存储指令和访问指令)完成之后,才执行该屏障之后的内存访问指令JMM采取保守的内存屏障插入策略:
在每个volatile写操作的前面插入一个StoreStore屏障。在每个volatile写操作的后面插入一个StoreLoad屏障。在每个volatile读操作的后面插入一个LoadLoad屏障。在每个volatile读操作的后面插入一个LoadStore屏障。因为有了上述非常保守的插入策略才可以保证其在任意处理器平台,任意程序都能得到正确的volatile内存语义
所以它能保证单个volatile变量的读/写具有原子性
这里有一个面试题:ConcurrentHashMap 的 get 方法为什么不用加锁,会不会出现数据读写不一致情况呢?
答案是不会,因为变量 value 是由 volatile 修饰的,根据 JMM 中的 happens-before 规则保证了对于 volatile 修饰的变量始终是写操作先于读操作的,并且 volatile 的内存可见性保证修改完的数据可以马上更新到主存中,所以能保证在并发情况下,读出来的数据是最新的数据。
get 方法逻辑比较简单,只需要将 key 通过 hash 之后定位到具体的 Segment ,再通过一次 hash 定位到具体的元素上。
它还可以用在双重效验锁来实现的单例模式上:
public class Singleton { private volatile static Singleton singleton; private Singleton (){} public static Singleton getSingleton() { if (singleton == null) { synchronized (Singleton.class) { if (singleton == null) { singleton = new Singleton(); } } } return singleton; } }关键在于singleton = new Singleton(); 这行代码,对象的创建不是一步完成的,是一个复合操作,需要如下3个指令:
指令1:获取singleton对象的内存地址;
指令2:初始化singleton对象;
指令3:将这块内存地址,指向引用变量singleton。
但是由于JVM具有指令重排的特性,执行顺序有可能变成1-3-2,如下图:
线程0执行了1和3,此时1调用 getSingleton() 后发现 singleton 不为空,因此就返回了一个还没有被初始化singleton,从而致使程序发生错误,所以需要使用 volatile来保证此段代码在多线程下也能正常执行