双重检测加锁的安全性

双重检测加锁法

这种单例模式看起来是十分安全的,单例不会遭到破坏。但实际上他在高并发下,也有可能被破坏。首先我们需要了解指令重排的概念和volatile关键字的作用

指令重排

什么是指令重排?
在计算机执行指令的顺序在经过程序编译器编译之后形成的指令序列,一般而言,这个指令序列是会输出确定的结果;以确保每一次的执行都有确定的结果。但是,一般情况下,CPU和编译器为了提升程序执行的效率,会按照一定的规则允许进行指令优化,在某些情况下,这种优化会带来一些执行的逻辑问题,主要的原因是代码逻辑之间是存在一定的先后顺序,在并发执行情况下,会发生二义性,即按照不同的执行逻辑,会得到不同的结果信息。

简单理解指令重排就是:

1
2
3
int a = 10; //1
int b = 20; //2
int c = a * b; //3

这段代码。它在底层的执行顺序可能是1-2-3也可能是2-1-3。这是为什么呢?因为先执行1或者先执行2对其结果其实是没有影响的,因为其满足了as-if-serial语义。
as-if-serial语义的意思是:不管怎么重排序(编译器和处理器为了提高并行度),(单线程)程序的执行结果不会改变。

volatile

volatile两大作用

  1. 保证内存可见性
  2. 防止指令重排

此外需注意volatile并不保证操作的原子性。

分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class SinglePattern5 {
//私有静态变量
private static SinglePattern5 single;
//私有构造方法
private SinglePattern5() {
// TODO Auto-generated constructor stub
}
//公共静态的获取实例的方法
public static SinglePattern5 getInstance() {
//双重检测加锁法 //一
if (single == null)
synchronized (SinglePattern5.class) {
if(single == null) {
single = new SinglePattern5(); //二
}
}
return single; //三
}
}

single = new SinglePattern5();这一行代码,并不是原子性的。它可以拆分为三个步骤:

  1. 给single分别地址。
  2. 执行SinglePattern5构造方法,创建一个新的对象。
  3. single指向新对象(此时single!=null)。

前提:线程执行这三行指令的时候,执行顺序可能是1-2-3。但是根据指令重排的规则可能变成1-3-2。
假设:线程A,开始执行single=new SinglePattern5()。执行的顺序是1-3-2。但是在执行3的时候线程休眠了。然后线程B开始得到执行权,在最外层判断single==null时为false。导致getInstance()方法直接返回了single。并且假设此时在外部调用了单例对象的方法,此时就会报空指针的异常! 这就是双重检测加锁法不安全的地方。

该如何改进?

前面已经介绍到,volatile关键字的作用有防止指令重排的作用。我们只需要在定义变量的时候加上该关键字就可以防止上面这种情况发生了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class SinglePattern5 {
//私有静态变量
private volatile static SinglePattern5 single;
//私有构造方法
private SinglePattern5() {
// TODO Auto-generated constructor stub
}
//公共静态的获取实例的方法
public static SinglePattern5 getInstance() {
//双重检测加锁法
if (single == null)
synchronized (SinglePattern5.class) {
if(single == null) {
single = new SinglePattern5();
}
}
return single;
}
}

-------------本文结束感谢您的阅读-------------