单例模式之双重校验锁(double checked locking)

264 阅读3分钟

引言

通过单例模式,我们能够保证获取到的对象始终都是同一个对象。单例模式又有两种写法:饿汉式和懒汉式。

饿汉式

我们先来看看饿汉式的单例模式是怎么编写的.

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) {
        	instance = new Singleton();
        }
        return instance;
    }
}

如果不加思考,我相信很多人都会将懒汉式的单例模式写成上面这个样子。认真思索一下后,我们很容易发现这样是线程不安全的。当有多个线程同时执行getInstance方法时,就会出现多次实例化对象的情况。

同步的懒汉式(效率低)

很自然的,我们就想到使用synchronized关键字来实现同步。因此,我们的代码就变成下面这个样子:

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) {
        	synchronized(Singleton.class) {
            	if(instance == null) {
                	instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

看上去,上面的代码好像已经没什么问题,但实际上还存在一个很大的隐患。 instance = new Singleton(); 这行实例化对象的代码,其实可以分为三个步骤:

  1. 分配内存空间
  2. 初始化对象
  3. 将对象指向刚刚分配的内存

但实际上,有些编译器为了提高效率,可能会将代码重排序,变成下面的顺序

  1. 分配内存空间
  2. 将对象指向刚刚分配的内存
  3. 初始化对象

接下来分析一下代码重排序后的运行状况:

TimeThread1Thread2
T1检查到instance为空
T2获取锁
T3为instance分配内存空间
T4将instance对象指向刚刚分配的内存空间
T5检查到instance不为空
T6访问instance实例(此时instance尚未初始化)
T7初始化instance对象

我们可以看到,在多线程的情况下,可能会出现一个线程获取到的是未初始化的对象。

双重校验锁(最终版)

为了保证线程能够获取到最终初始化的对象,我们需要使用volatile关键字,禁止代码的重排序,从而解决上面的问题。

public class Singleton {
	
    private static volatile Singleton instance;
    
    private Singleton() {}
    
    public static Singleton getInstance() {
    	if(instance == null) {
        	synchronized(Singleton.class) {
            	if(instance == null) {
                	instance = new Singleton();
                }
            }
        }
        return instance;
    }
}