这篇博客主要介绍单例模式中的双重验证中是否需要加volatile,以及为什么。
public class Singleton {
private static Singleton instance = new Singleton();
private Singleton() {}
public static Singleton getInstance() {
return instance;
}
}
这种写法无论在单线程还是多线程环境下都不会出现任何安全性问题。但是,这种实现方式有一个缺点:无论这个单例是否被使用,都会在内存中创建一个这样的单例。所以出现了后续的懒加载实现方式。
public class Singleton {
private static Singleton instance;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) { // 1
instance = new Singleton();
}
return instance;
}
}
懒加载即使用单例的时候才初始化。但是,这种实现方式有一个明显的缺点:当在多线程环境下,多个线程同时运行到代码1处时,instance 为null,这几个线程都会创建自己的单例,而不是使用的同一个单例对象。如果给getInstance()方法加上同步关键字呢?
public class Singleton {
private static Singleton instance;
private Singleton() {}
public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
方法加锁的实现方式自然能保证多线程环境下的安全性,但是方法加锁的方式会严重影响性能。接下来考虑细粒度的加锁方式——代码块加锁。
public class Singleton {
private static Singleton instance;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) { // 1
synchronized (Singleton.class) {
instance = new Singleton();
}
}
return instance;
}
}
但是这种细粒度的加锁方式并不能保证多线程环境下的安全性。举例说明:A,B两个线程同时运行到代码1处,接下来两个线程会竞争Singleton类锁。假如A线程获得了锁,A线程继续执行,直到A线程创建一个Singleton实例对象,A线程释放锁。这时B线程获取到锁,依然是继续执行,此时B线程仍然会创建一个Singleton实例对象。A,B两个线程就创建了两个不同的Singleton实例对象。接下来继续改进,如果在同步代码块中再加一层check,check instance是否为null——双重验证(DCL —— Double Check Lock)。
public class Singleton {
private static Singleton instance;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) { // 1
synchronized (Singleton.class) {
if (instance == null) { // 2
instance = new Singleton();
}
}
}
return instance;
}
}
根据上面的解析,A线程执行完毕释放锁后,B线程获取到锁时,再次检查instance是否为Null。由于synchronized可以保证线程可见性,所以A线程中对instance赋值后会将值刷新到主存中去,并且导致所有线程中关于instance的线程本地缓存值都会失效。B线程运行到代码2处时,B线程的instance在线程本地缓存中的值已经失效,所以会重新去主存中去拿,这时B线程在代码2处的返回值为false,所以B线程不再创建新的对象,而直接返回。双重验证总归可以了吧?答案还是No。这是很多人认为理所当然的,感觉经得起推敲。但是,如果读者了解对象的创建过程,并且知道在多线程环境下,线程有可能会使用一个未完全初始化的对象,就会明白为什么这种方案还是不行。接下来大概描述一下对象的创建过程,以及什么是未完全初始化的对象。
public class TestNewObj {
public static void main(String[] args) {
T t = new T();
}
}
class T {
int m = 10;
}
javac ./TestNewObj.java 得到class文件
javap -c ./TestNewObj.class 查看字节码指令,本例如下:
创建对象的指令就在红色圈中。
对象的创建过程(new Object()),根据如上java代码进行如下描述:
这3个步骤分别对应上图指令中的 0, 4, 7。
public class Singleton {
private static Singleton instance;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) { // 1
synchronized (Singleton.class) {
if (instance == null) { // 2
instance = new Singleton();
}
}
}
return instance;
}
}
然后回想一下DCL单例中,如果存在这样一种情形:
两个线程A,B同时执行到代码1处,线程A先获取到锁,当线程A创建对象T,指令执行到指令0时(此时m = 0),发生了指令重排序,指令4和指令7位置互换,即由之前的执行顺序4->7变成了7->4。此时发生指令重排时,会将一个半初始化的对象与 t 建立关联。所以当线程B获取到锁时会再次判断instance是否为null,此时instance已经不为null了,就直接用了一个半初始化状态的对象,m = 0,这就是其安全性问题所在。要问这个情况如何验证,抱歉,以博主的水平做不到,但是相信如果有阿里那样的并发量,一定会出现。
根本原因在于,对象的创建不是原子性操作,所以有指令重排序的可能。为了禁止指令重排序,所以要引入volatile。终于点题了—— DCL单例模式需不需要volatile?为什么?
答案是肯定的,需要volatile。
public class Singleton {
private static volatile Singleton instance;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) { // 1
synchronized (Singleton.class) {
if (instance == null) { // 2
instance = new Singleton();
}
}
}
return instance;
}
}
所以volatile在DCL单例中不是使用它的线程可见性,而是禁止指令重排序。
对象的创建不是原子性操作,所以有指令重排序的可能。为了禁止指令重排序,所以要引入volatile。
本篇博客用于记录学习,仅供参考。如有分析不当的地方,欢迎各位大佬指出。
参考:https://www.bilibili.com/video/BV1kT4y177jE