线程安全的单例模式
About 3 min
单例模式是设计模式中使用最为普遍的模式之一。它是一种对象创建模式,用于产生一个对象的具体实例,它可以确保系统中一个类只产生一个实例。单例与多线程没啥直接关系,但是保证多线程下单例模式的安全性,是件有趣且好玩的事情。今天我们就来聊聊这单例。
单例模式
饿汉模式
public class Singleton {
private final static Singleton INSTANCE = new Singleton();
private Singleton() {}
public static Singleton getInstance() {
return INSTANCE;
}
}
这种单例模式是线程安全的,没有加锁。但是对于变量INSTANCE
会在类的首次初始化时被创建。
懒汉模式
public class Singleton {
private static Singleton INSTANCE;
private Singleton() {}
public static Singleton getInstance() throws InterruptedException {
if (INSTANCE == null) {
TimeUnit.SECONDS.sleep(2); // 模拟一些准备的耗时操作
INSTANCE = new Singleton();
}
return INSTANCE;
}
}
这种会有问题的,当多个线程同时执行到第7行,都判断INSTANCE
没有被初始化,然后多个线程都进行了初始化,从而生成了多个实例。
延迟加载解决方案之声明synchronized
public class Singleton {
private static Singleton INSTANCE;
private Singleton() {}
public synchronized static Singleton getInstance() throws InterruptedException {
if (INSTANCE == null) {
TimeUnit.SECONDS.sleep(2); // 模拟一些准备的耗时操作
INSTANCE = new Singleton();
}
return INSTANCE;
}
}
这种是线程安全的,因为getInstance
方法加了synchronized
,所以保证了这个方法只能同时被一个线程访问。
延迟加载解决方案之同步代码块
public class Singleton {
private static Singleton INSTANCE;
private Singleton() {}
public static Singleton getInstance() throws InterruptedException {
synchronized (Singleton.class) {
if (INSTANCE == null) {
TimeUnit.SECONDS.sleep(2); // 模拟一些准备的耗时操作
INSTANCE = new Singleton();
}
}
return INSTANCE;
}
}
这种是线程安全的,和上面的写法很类似。synchronized
加在了要保护的代码块上,保证了判断和初始化逻辑是线程安全了。
延迟加载解决方案之同步部分代码块
public class Singleton {
private static Singleton INSTANCE;
private Singleton() {}
public static Singleton getInstance() throws InterruptedException {
if (INSTANCE == null) {
TimeUnit.SECONDS.sleep(2); // 模拟一些准备的耗时操作
synchronized (Singleton.class) {
INSTANCE = new Singleton();
}
}
return INSTANCE;
}
}
这种是线程不安全的
延迟加载解决方案之DCL双检查锁机制
public class Singleton {
private static volatile Singleton INSTANCE;
private Singleton() {}
public static Singleton getInstance() throws InterruptedException {
if (INSTANCE == null) {
TimeUnit.SECONDS.sleep(2); // 模拟一些准备的耗时操作
synchronized (Singleton.class) {
if (INSTANCE == null) {
INSTANCE = new Singleton();
}
}
}
return INSTANCE;
}
}
这种是线程安全的,INSTANCE
采用volatile
关键字修饰也是很有必要的。
INSTANCE = new Singleton();
这段代码其实是分为三步执行
- 为
INSTANCE
分配内存空间 - 初始化
INSTANCE
- 将
INSTANCE
指向分配的内存地址
但是由于JVM具有指令重排的特性,执行顺序有可能变成1->3->2
。指令重排在单线程环境下不会出现问题,但是在多线程环境下会导致一个线程获得还没有初始化的实例。例如,线程T1执行了1和3,此时T2 调用getInstance()
后发现INSTANCE
不为空,因此返回INSTANCE
,但此时INSTANCE
还未被初始化。