目录
介绍
Volatile保证可见性的原理
可见性问题
原理
Volatile保证有序性的原理
指令重排
内存屏障
如何解决volatile不保证原子性问题?
由Volatile解决的单例模式中双重检索问题(DCL)
介绍
volatile 是 Java 虚拟机提供的轻量级的同步机制(三大特性)
-
保证可见性
-
保证有序性(禁止指令重排)
-
不保证原子性
性能:volatile 修饰的变量进行读操作与普通变量几乎没什么差别,但是写操作相对慢一些,因为需要在本地代码中插入很多内存屏障来保证指令不会发生乱序执行,但是开销比锁要小
synchronized 无法禁止指令重排和处理器优化,为什么可以保证有序性和可见性
加了锁之后,只能有一个线程获得到了锁,获得不到锁的线程就要阻塞,所以同一时间只有一个线程执行,相当于单线程,由于数据依赖性的存在,单线程的指令重排是没有问题的
线程加锁前,将清空工作内存中共享变量的值,使用共享变量时需要从主内存中重新读取最新的值;线程解锁前,必须把共享变量的最新值刷新到主内存中(JMM 内存交互章节有讲)
Volatile保证可见性的原理
可见性问题
可见性问题指的是一个线程在访问一个共享变量的时候,其他线程对该共享变量的修改对于第一个线程来说是不可见的。
在Java中造成可见性问题的原因是Java内存模型(JMM),在Java内存模型中,规定了共享变量是存放在主内存
中,然后每个线程都有自己的工作内存
,而线程对共享变量的操作,必须先从主内存中读到工作内存中去,至于什么时候写回到主内存是不可预知的,这就导致每个线程之间对共享变量的操作是封闭的,其他线程不可见的。
通过volatile修饰的变量,当一个线程改变了该变量的值,会将共享变量值立即刷新回主内存。而线程读取共享变量必须从主内存中读取,这样就实现了并发下共享资源的可见性。
原理
volatile 修饰的变量,汇编指令中会存在一个lock指令的前缀
。会将当前处理器缓存行的数据写回到系统内存,同时触发缓存一致性协议,使在其他CPU里缓存了该内存地址的数据无效。
对声明了volatile关键字的变量进行写操作,JVM会向处理器发送一条lock前缀的指令,将这个变量所在的缓存行立即写回系统内存。并且为了保证各个处理器的缓存是一致的,实现了缓存一致性协议
,各个处理通过嗅探
在总线上传播的数据来检查自己缓存的值是不是过期了,当处理器发现自己缓存行对应的内存地址被修改,就会将当前处理器的缓存行设置成无效状态
,那么下次对这个数据进行操作,就会重新从系统内存中获取最新的值。对应JMM来说就是:
Lock前缀的指令让线程工作内存中的值写回主内存中;
通过缓存一致性协议,其他线程如果工作内存中存了该共享变量的值,就会失效;
其他线程会重新从主内存中获取最新的值;
Volatile保证有序性的原理
指令重排
指令重排序是指编译器或处理器为了优化程序性能而重新排列指令执行顺序的一种技术,对于一些与执行顺序没有关联的语句,CPU就可能会进行指令重排,在单线程下,这是没有问题的,但是在并发的情况下,就可能会造成指令执行顺序出错而导致的情况
指令重排实例:
-
ex1:
public void mySort() {int x = 11; //语句1int y = 12; //语句2 x = x + 5; //语句3y = x * x; //语句4 }
执行顺序可能是:1 2 3 4、2 1 3 4、1 3 2 4
指令重排也是有限制的,不会出现:4 3 2 1,语句 4 需要依赖于 y 以及 x 的申明,因为存在数据依赖,无法首先执行,在单线程下,不会出现问题
-
ex2:
int num = 0; boolean ready = false; // 线程1 执行此方法 public void actor1(I_Result r) {if(ready) {r.r1 = num + num;} else {r.r1 = 1;} } // 线程2 执行此方法 public void actor2(I_Result r) {num = 2;ready = true; }
情况一:线程 1 先执行,ready = false,结果为 r.r1 = 1
情况二:线程 2 先执行 num = 2,但还没执行 ready = true,线程 1 执行,结果为 r.r1 = 1
情况三:线程 2 先执行 ready = true,线程 1 执行,进入 if 分支结果为 r.r1 = 4
情况四:线程 2 执行 ready = true,切换到线程 1,进入 if 分支为 r.r1 = 0,再切回线程 2 执行 num = 2,发生指令重排。
Volatile通过内存屏障的方式解决指令重排序问题
内存屏障
内存屏障分为以下4类:
屏障类型 | 指令示例 | 说明 |
---|---|---|
LoadLoad Barries | Load1;LoadLoad;Load2 | 确保Load1数据的装载先于Load2以及后续装载指令的装载。 |
StoreStore Barries | Store1;StoreStore;Store2 | 确保Store1数据刷新到内存先于Store2以及后续存储指令的存储。 |
LoadStore Barries | Load1;LoadStore;Store2 | 确保Load1数据的装载先于Store2数据刷新到内存以及后续存储指令的存储。 |
StoreLoad Barries | Store1;StoreLoad;Load2 | 确保Store1数据刷新到内存先于Load2数据的装载以及后续装载指令的装载。 |
在每个volatile写操作的前面插入一个StoreStore屏障。
在每个volatile写操作的后面插入一个StoreLoad屏障。
在每个volatile读操作的后面插入一个LoadLoad屏障。
在每个volatile读操作的LoadLoad屏障后面插入一个LoadStore屏障。
volatile写操作的内存屏障示意图:
volatile读操作的内存屏障示意图:
如何解决volatile不保证原子性问题?
-
在方法上加入 synchronized,虽然能够保证原子性,但是为了解决number++,而引入重量级的同步机制不太值得。
-
如何不加synchronized解决number++在多线程下是非线程安全的问题?使用AtomicInteger(前面CAS内容中有介绍)。
由Volatile解决的单例模式中双重检索问题(DCL)
不使用Volatile修饰变量
public class Singleton {private static Singleton instance = null;private Singleton(){ }public static Singleton getInstance() {if(instance == null) {synchronized(Singleton.class) {if(instance == null) {//这里可能会出现指令重排序问题instance = new Singleton();}}}return instance;}}
Instance=new Singleton()可以分为3步
//创建一个变量instance分为3步
//第一步:分配内存空间
memory = allocate();
//第二步:初始化对象
NewInstance(memory);
//第三步:设置instance指向刚分配的内存地址
instance = memory;
其中第一步的顺序是不会发生变化的,但是第2、3步功能跟他们的顺序是没有关系的,在单线程下,哪一个先执行都可以
//创建一个变量instance分为3步
//第一步:分配内存空间
memory = allocate();
//第三步:设置instance指向刚分配的内存地址
instance = memory;
//第二步:初始化对象
NewInstance(memory);
在是在多线程并发情况下:如果一个线程A由于指令重排,在设置instance指向刚分配的内存地址但还未初始化对象时,注意此时instance已经不为null了,正好另外一个线程B调用该方法,将会获得一个未初始化完毕的单例。
加入Volatile之后的DCL代码:
public class Singleton {//如果不使用volatile,也可以使用静态内部类的方式保证单例性private static volatile Singleton instance = null;private Singleton(){ }public static Singleton getInstance() {if(instance == null) {synchronized(Singleton.class) {if(instance == null) {instance = new Singleton();}}}return instance;}}
静态内部类保证单例性
public class SingletonDemo {private SingletonDemo() { }private static class SingletonDemoHandler {private static SingletonDemo instance = new SingletonDemo();}public static SingletonDemo getInstance() {return SingletonDemoHandler.instance;}
}
因为类加载本身就是懒惰的,在没有调用getInstance方法时是没有执行SingletonDemoHandler内部类的类加载操作的。静态内部类不会随着外部类的加载而加载, 这是静态内部类和静态变量的区别。同时也不会有并发问题,因为是通过类加载创建的单例, 由JVM保证不会出现线程安全。