1、volatile 概念
volatile 是 java 虚拟机提供的轻量级同步机制
volatile 三个特性:
- 保证可见性
- 不保证原子性
- 禁止指令重排
2、volatile 禁止指令重排
(1)指令重排有序性:
计算机在执行程序时,为了提高性能,编译器和处理器常常会做指令排重,一般分为以下三种:
单线程环境里面确保程序最终执行结果和代码顺序执行结果一致。
处理器在进行指令重排序时必须考虑指令之间的数据依赖性
多线程环境中线程交替执行,由于编译器指令重排的存在,两个线程使用的变量能否保证一致性是无法确认的,结果无法预测。
指令重排案例分析 one:
public void mySort() {
int x = 11; // 语句 1
int y = 12; // 语句 2
x = x + 5; // 语句 3
y = x * x; // 语句 4
}
// 指令重排之后,代码执行顺序有可能是以下几种可能?
// 语句 1 → 语句 2 → 语句 3 → 语句 4
// 语句 1 → 语句 3 → 语句 2 → 语句 4
// 语句 2 → 语句 1 → 语句 3 → 语句 4
// 问题:请问语句 4 可以重排后变为第 1 条吗?
// 不能,因为处理器在指令重排时必须考虑指令之间数据依赖性。
指令重排案例分析 two:
指令重排案例分析 three:
public class BanCommandReSortSeq {
int a = 0;
boolean flag = false;
public void methodOne() {
a = 1; // 语句 1
flag = true; // 语句 2
// methodOne 发生指令重排,程序执行顺序可能如下:
// flag = true; // 语句 2
// a = 1; // 语句 1
}
public void methodTwo() {
if (flag) {
a = a + 5; // 语句 3
}
System.out.println(“methodTwo ret a =” + a);
}
// 多线程环境中线程交替执行,由于编译器指令重排的存在,两个线程使用的变量能否保证一致性是无法确认的,结果无法预测。
// 多线程交替调用会出现如下场景:
// 线程 1 调用 methodOne,如果此时编译器进行指令重排
// methodOne 代码执行顺序变为:语句 2(flag=true) → 语句 1(a=5)
// 线程 2 调用 methodTwo,由于 flag=true,如果此时语句 1 还没有执行(语句 2 → 语句 3 → 语句 1 ),那么执行语句 3 的时候 a 的初始值 = 0
// 所以最终 a 的返回结果可能为 a = 0 + 5 = 5,而不是我们认为的 a = 1 + 5 = 6;
}
(2)禁止指令重排底层原理:
volatile 实现禁止指令重排优化,从而避免多线程环境下程序出现乱序执行的现象
先了解下概念,内存屏障(Memory Barrier)又称内存栅栏,是一个 CPU 指令,它的作用有两个:
- 保证特定操作执行的顺序性
- 保证某些变量的内存可见性(利用该特性实现 volatile 内存可见性)
volatile 实现禁止指令重排优化底层原理:
由于编译器和处理器都能执行指令重排优化。如果在指令间插入一条 Memory Barrier 则会告诉编译器和 CPU,不管什么指令都不能和这条 Memory Barrier 指令重排,也就是说通过插入内存屏障,就能禁止在内存屏障前后的指令执行重排优化。内存屏障另外一个作用就是强制刷出各种 CPU 的缓存数据,因此任何 CPU 上的线程都能读取到这些数据的最新版本。
左边:写操作场景:先 LoadStore 指令,后 LoadLoad 指令。
右边:读操作场景:先 LoadLoad 指令,后 LoadStore 指令。
3、volatile 使用场景
单例模式 (DCL-Double Check Lock 双端检锁机制)
常见的 DCL(Double Check Lock)模式虽然加了同步,但是在多线程下依然会有线程安全问题。
public class SingletonDemo {
private static SingletonDemo singletonDemo=null;
private SingletonDemo(){
System.out.println(Thread.currentThread().getName()+"\t 我是构造方法");
}
//DCL模式 Double Check Lock 双端检索机制:在加锁前后都进行判断
public static SingletonDemo getInstance(){
if (singletonDemo==null){
synchronized (SingletonDemo.class){
if (singletonDemo==null){
singletonDemo=new SingletonDemo();
}
}
}
return singletonDemo;
}
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
new Thread(()->{
SingletonDemo.getInstance();
},String.valueOf(i+1)).start();
}
}
}
这个漏洞比较 tricky,很难捕捉,但是是存在的。instance=new SingletonDemo();
可以大致分为三步
memory = allocate(); //1.分配内存
instance(memory); //2.初始化对象
instance = memory; //3.设置引用地址
其中 2、3 没有数据依赖关系,可能发生重排。如果发生,此时内存已经分配,那么instance=memory
不为 null。如果此时线程挂起,instance(memory)
还未执行,对象还未初始化。由于instance!=null
,所以两次判断都跳过,最后返回的instance
没有任何内容,还没初始化。
解决的方法就是对singletondemo
对象添加上volatile
关键字,禁止指令重排。