扫盲细节,到底该如何正确地写出单例模式?
单例模式算是设计模式中最容易理解,也是最容易手写代码的模式,但是其中的坑却不少,很多都是一些老生常谈的问题,如何创建一个线程安全的单例?什么是双检锁?我们知道单例模式一般分两种,即懒汉式和饿汉式,以下逐一分析。
懒汉式,线程不安全
|
这段代码简单明了,而且使用了懒加载,但是却存在致命的问题。当有多个线程并行调用 getInstance() 的时候,就会创建多个实例,也就是说在多线程下不能达到仅存在一个实例的效果。
懒汉式,线程安全
为了解决上面的问题,最简单的方法是将getInstance() 方法设为同步(synchronized)。
|
虽然做到了线程安全,解决了多实例的问题,但它并不高效。
双重检验锁模式实现单例
双重检验锁模式是一种使用同步代码块加锁的方法,会有两次检查instance==null,一次在同步块外,一次在同步块内。那为什么在同步块内还要校验一次呢?是因为可能会有多个线程一起进入同步块外的if,如果在同步块内不进行二次校验的话就可能出现多个实例。
public static Singleton getInstance() {
if (singleton == null) {
synchronized (Singleton.class) {
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}复制代码
很遗憾,以上方式也不是很完美,问题在于singleton
= new Singleton()
这段代码,这并非是一个原子操作,事实上在 JVM 中对这段代码大概做了3 件事:
- 给singleton分配内存
- 调用Singleton的构造函数来初始化成员变量
- 将singleton对象指向分配的内存空间
但是在JVM的即时编译器中存在指令重排序的优化,也就是上面的第二步和第三步的执行顺序得不到保证,最终执行顺序可能是1-2-3也可能是1-3-2,如果是后者的话,则3执行完毕且2未执行之前,getInstance()被其他线程调用,这时singleton已经不是null,但却没有初始化直接返回singleton然后使用,此时就会报错。为了解决这个问题,我们需要将singleton声明为volatile就行了。
public class Singleton {
private static volatile Singleton singleton;
private Singleton() {}
public static Singleton getInstance() {
if (singleton == null) {
synchronized (Singleton.class) {
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
}复制代码
使用volatile不仅仅是保证线程在本地不会有singleton副本,每次去内存中读取,还有另一个重要特性:禁止指令重排序优化。
饿汉式
这种方式很简单,单例的实例被声明成了static和final,在第一次加载到内存中就会被初始化,所以创建的实例本身是线程安全的。
public class Singleton {
public static final Singleton singleton = new Singleton();
private Singleton() {}
public static Singleton getInstance() {
return singleton;
}
}复制代码
饿汉式的缺点是它不是懒加载模式,单例会在加载类后一开始就被初始化。且这种模式在某些场景下无法使用,比如Singleton实例的创建时依赖参数或者配置文件,在getInstance()之前必须调用某个方法设置参数。