Fork me on GitHub

深度分析如何写出一个线程安全的单例

封面

什么是单例模式?

单例模式是在Java编程中除了工厂模式之外最常用的创建型设计模式之一。单例模式提供了一种创建对象的方式,使得每次获取到的该类的实例都是同一个。即所谓的提供了访问该类实例的唯一途径。

单例模式在创建时的注意事项:

  • 因为每个类只能创建一个实例,所以需要将其构造方法封闭起来不能被外部调用,即私有化;
  • 需要提供一个获取类实例的公有方法,所有使用该类实例的人都通过这个方法拿到单例对象;
  • 在多线程环境下需要注意线程安全问题,避免多个线程同时创建出多个不同的实例,违背了单例的原则。

创建单例模式的方法及其优缺点

饿汉型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class HungrySingleton {
private static final HungrySingleton INSTANCE = new HungrySingleton();

/**
* 私有构造
*/
private HungrySingleton() {}

/**
* 获取类在加载的时候就创建好的实例
*/
public static HungrySingleton getInstance() {
return INSTANCE;
}
}
  • 分析:饿汉型单例是最简单的单例创建方法,在类中维护一个该类私有不可变的实例,然后提供一个获取该实例的静态方法即可。
  • 优点:代码编写简单,线程安全(使用类加载机制保证线程安全,classloader在加载类的时候使用synchronized同步)
  • 缺点:无法实现懒加载,在使用较少的时候浪费资源,无法防止反射破坏以及反序列化破坏单例唯一性

普通懒汉型

1
2
3
4
5
6
7
8
9
10
11
12
13
public class LazySingleton {

private static LazySingleton INSTANCE ;

private LazySingleton(){}

public static LazySingleton getInstance(){
if(null == INSTANCE){
INSTANCE = new LazySingleton();
}
return INSTANCE;
}
}
  • 分析:这种懒汉型单例也是一种比较简单的单例实现方式,与第一种不同在于这种实例是在调用获取实例的静态方法的时候才创建
  • 优点:代码简单,可以实现懒加载
  • 缺点:多线程环境下会有线程安全问题,多个线程同时走到第8行代码,判断实例未创建,则会创建出多个实例;无法防止反射破坏以及反序列化破坏

那么为了实现线程安全我们可以在方法上加上同步关键字,但是这种方式的缺点就是多线程环境调用该方法都会阻塞,导致性能存在不必要的浪费

1
2
3
4
5
6
public synchronized static LazySingleton getInstance(){
if(null == INSTANCE){
INSTANCE = new LazySingleton();
}
return INSTANCE;
}

双重检测锁实现懒加载单例

上述懒汉型同步机制可以改进如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class DoubleLockSingleton {

private DoubleLockSingleton() {
}

private static DoubleLockSingleton INSTANCE;

public static DoubleLockSingleton getInstance() {
// 进入方法后先判断一次 大幅减少多线程环境同步阻塞问题
// 但是这种方式在多线程第一次调用创建实例的时候会出现线程安全问题
if (null == INSTANCE) {
synchronized (DoubleLockSingleton.class) {
if (null == INSTANCE) {
// 问题的根源所在
// 此处可能会出现指令重排序
// new对象并不是原子操作
INSTANCE = new DoubleLockSingleton();
}
}
}
return INSTANCE;
}
}
  • 分析:双重检测锁机制在进入getInstance()方法的时候会判断实例是否被创建,如果被创建,那么直接返回,如果没有,进入同步代码块,创建实例。这个方法貌似没有问题,而且还大大减少线程进入同步代码块阻塞的情况(因为只有第一次判断对象还没有创建的时候才会有线程进入同步代码块)。但是这种方法仍然存在线程安全性问题。问题出在第17行:INSTANCE = new DoubleLockSingleton();由于JVM虚拟机内部会对代码进行优化,在使用new创建对象的时候并不是一个原子操作且会被虚拟机进行指令重排序。这个步骤会被分成三步:1、在堆上为对象分配空间;2、对象进行初始化;3、将引用指向该堆上的地址。 由于在虚拟机中,指令重排序优化导致第2步和第3步的执行顺序可以被打乱,那么在代码执行到第11行的时候发现,INSTANCE不为null,直接返回一个未初始化完成的对象,导致程序崩溃。注意:synchronzed关键字没有屏蔽指令重排序的功能,那么如何优化呢?答案是使用volatile关键字修饰实例变量引用,即private volatile static TripleLockSingleton INSTANCE;volatile关键字才有屏蔽指令重排序的语义。
  • 优点:多线程环境下大部分时间线程安全
  • 缺点:有可能会出现线程安全问题;不能避免反射和反序列化破坏
  • 优化:private volatile static TripleLockSingleton INSTANCE;

静态内部类单例

1
2
3
4
5
6
7
8
9
10
11
12
public class StaticInnerClassSingleton {
private StaticInnerClassSingleton() {
}

private static class SingletonHolder {
public static final StaticInnerClassSingleton INSTANCE = new StaticInnerClassSingleton();
}

public static StaticInnerClassSingleton getInstance() {
return SingletonHolder.INSTANCE;
}
}
  • 分析:静态内部类实现单例也是借助了类加载的线程安全机制,并同时基于内部类的使用时再创建的懒加载机制实现线程安全的懒加载模式的单例
  • 优点:线程安全,可以实现懒加载
  • 缺点:无法防止被反射以及反序列化破坏单例的唯一性

枚举类单例

1
2
3
4
5
6
7
8
public enum EnumSingleton {

INSTANCE;

public static EnumSingleton getInstance() {
return INSTANCE;
}
}
  • 分析:Joshua Bloch大神说过:“单元素的枚举类型已经成为实现Singleton的最佳方法”。足以见证枚举类在创建单例中的优势。
  • 优点:代码简单,线程安全,可以防止反射和反序列化破坏
  • 缺点:暂无

CAS创建单例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class CASSingleton {

private static final AtomicReference<CASSingleton> INSTANCE = new AtomicReference<>();

private CASSingleton() {}

/**
* 使用原子操作 实现获取唯一实例
* 理论上在大量竞争的环境中 原子操作自旋等待消耗大量性能 但是实际上当一个线程创建好实例之后其余线程不会出现死循环
* 相对于锁它没有线程切换和阻塞的额外消耗,可以支持较大的并行度
*/
public static CASSingleton getInstance() {
for (; ; ) {
CASSingleton instance = INSTANCE.get();
if (null != instance) {
return instance;
}
instance = new CASSingleton();
if (INSTANCE.compareAndSet(null, instance)) {
return instance;
}
}
}
}
  • 分析:使用原子操作AtomicReference进行单例的创建,事实上目前相对于双重检查锁性能上差的也不是也别大
  • 优点:线程安全,懒加载,相对于锁它没有线程切换和阻塞的额外消耗,可以支持较大的并行度
  • 缺点:代码编写相对复杂,不能防止被反射和反序列化破坏

防止反射和反序列化破坏单例的方式

上述几个创建单例的方式中,除了使用枚举类创建单例,都会产生被反射和反序列化破坏的情况,避免该问题的方式如下,大家需要深究的话可以打开搜索引擎… 这里不再展开了~

枚举类防止反射和反序列化破坏

参考文章:https://www.cnblogs.com/chiclee/p/9097772.html

防止反射破坏

因为反射是通过class对象来调用类的构造方法创建对象的,我们只需要在构造方法中进行判断,如果实例已经存在,就抛出异常。

1
2
3
4
5
6
7
8
/**
* 私有构造
*/
private Singleton() {
if(INSTANCE != null){
throw new RuntimeException();
}
}

防止反序列化破坏

在反序列化的时候ObjectInputStream.readObject()中会去判断是否存在readResolve()方法,如果存在的话会调用该方法返回一个实例,所以在单例类中编写readResolve()方法返回INSTANCE即可。

1
2
3
4
// 在反序列化时,直接调用这个方法,返回指定的对象,无需再新建一个对象
private Object readResolve() {
return INSTANCE;
}
陈年风楼 wechat
Add my WeChat, share tech-skills to each other 🙆‍