在描述volatile的作用时往往会采用并发情况下单例对象的创建的例子进行举例 下方代码为通过双重检锁机制实现单例
package com.lch; public class Singleton { public static volatile Singleton singleton; /** * 构造函数私有,防止外部实例化 */ private Singleton() {}; public static Singleton getInstance() { if (singleton == null) { synchronized (singleton) { if (singleton == null) { singleton = new Singleton(); } } } return singleton; } }
首先分析一下,实例化一个对象往往分为三个步骤:
-
分配内存空间
-
初始化对象
-
将内存地址赋值给对应的引用
但由于操作系统的指令操作可能会进行重排序,变成
-
分配内存空间
-
将内存地址赋值给对应的引用
-
初始化对象 那么多线程环境下将一个未初始化的对象引用暴露出来就有可能导致实例化的对象被覆盖,为了防止重排序就需要加上volatile关键字
原理实现
-
可见性
-
有序性
-
内存屏障
可见性
由于线程本身并直接与主存(主内存)进行数据交互,而是通过线程的工作内存来完成相应操作;
从而产生不同线程间数据不可见的问题。因此volatile实现可见性的原理就是:当修改volatile变量是强制将修改后的值刷新到主存中;
修改volatile变量后会导致其他线程工作内存中对应的变量值失效,因此在读取变量时需要重新从主存中获取
有序性和内存屏障
谈到有序性,就要说到Java中的happen-before语义规则,简单来说就是两个事件的发生,后续的事件可以看到前一个事件做的任何操作;为实现这一点volatile采用的是内存屏障的方式进行控制
屏障类型
屏障类型 | 操作 | 用途 |
---|---|---|
LoadLoad | 一个处理器先执行L1的读操作,再执行L2的读操作,其他处理器看到的是先L2后L1 | 确保L1数据的读先于L2 |
StoreStore | 一个处理器先执行w1写操作,再执行w2写操作,其他处理器看到的是先w2后w1 | 确保w1对数据的写操作对其他处理器可见(刷新到主存中)先于w2 |
LoadStore | 一个处理器先执行L1读操作,再执行L2写操作,其他处理器看到先w2后L1 | 确保L1数据的读先于w2的写操作写到主存之前 |
StoreLoad | 一个处理器先执行w1写操作,再执行L2读操作,其他处理器看到先L2后w1 | 确保w1对数据的写操作对其他处理器可见(刷新到主存中)先于L2对内存数据的读取 |
由此可以得出volatile的有序性主要是通过在变量读写操作过程中加入对应的内存屏障来禁止指令重排序,从而达到有序性。