「设计模式」使用更安全的单例模式

本文介绍 Java 单例模式,主要包括:

饿汉式单例实现

懒汉式单例简单实现

懒汉式线程安全加锁单例实现

懒汉式双重检查加锁单例实现。

使用枚举实现单例

饿汉式

饿汉式的特点就是空间换时间,在开始时就创建,而且只创建一次,节约了运行时间,保证了线程安全,缺点就是当你不需要用的时候也同样占用内存空间。

1
2
3
public class TokenProvider {
private static TokenProvider sInst = new TokenProvider();
}

懒汉式

懒汉式的特点就是时间换空间,不需要时不创建,需要时才创建,节约了内存空间,缺点就是每次获取都需要判断,增加了运行时间,而且不加锁的懒汉式无法保证线程安全。

看一个最简单的 饿汉式 创建单例的方法

1
2
3
4
5
6
7
8
9
10
11
public class TokenProvider {

private static TokenProvider sInst;

public static TokenProvider getInst() {
if (sInst == null) {
sInst = new TokenProvider();
}
return sInst;
}
}

为什么会有线程安全的问题呢?因为多线程访问时,假设 A 线程正在创建实体,此时 B 线程已经开始进行 sInst == null 的判空操作,此时 B 线程便会创建一个新的实体。

如何解决?请看下面线程安全加锁

线程安全加锁

当使用懒汉式时,如果多线程同时创建单例,不加锁的话就会创建多个实体,无法保证线程安全,此时应在判空操作之前加锁,让其他线程在外面等待已经进入的线程完成操作,创建完成实体,这时第二个线程进入时,sInst 已经不为空可以直接返回,从而保证线程安全。

加锁保证线程安全

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

private static TokenProvider sInst;

public static TokenProvider getInst() {
synchronized (TokenProvider.class) {
if (sInst == null) {
sInst = new TokenProvider();
}
}
return sInst;
}
}

缺点也很明显,需要使用该单例时,所有线程都会首先进入同步代码块,在线程同步时会浪费很多时间,我们需要避免这种情况,请看下节双重检查加锁

双重检查加锁

为了解决上面的问题,我们需要避免每次都进行同步加锁,在最外层先进行判空操作,当实体已经创建时,后面的线程则不需要进入同步代码块等待,节约了时间。

volatile: 被 volatile 修饰的变量的值,将不会被本地线程缓存,所有对该变量的读写都是直接操作共享内存,从而确保多个线程能正确的处理该变量。

我们必须使用 volatile 来避免指令重排的问题,简单的来说在 2 处创建对象时,为 sInst 分配内存空间 和 初始化 sInst 两条指令可能发生重排序,这会导致在 1 处获取到一个没有被初始化的对象导致问题发生。Double-Check-Locking 指令重排问题

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

private volatile static TokenProvider sInst;

public static TokenProvider getInst() {
if (sInst == null) { // 1
synchronized (TokenProvider.class) {
if (sInst == null) {
sInst = new TokenProvider(); // 2
}
}
}
return sInst;
}
}

使用枚举实现单例

简单说一下枚举,枚举类似类,一个枚举可以拥有成员变量,成员方法,构造方法。创建enum 时,编译器会自动为我们生成一个继承自 Java.lang.Enum 的类,构建实例的过程不是我们做的,一个 enum 的构造方法限制是 private 的,也就是不允许我们调用。单例中的每一个都是 static final 类型,下面代码的对比应该更清楚一些

1
2
3
enum Type{
A,B,C,D;
}

等同于

1
2
3
4
5
6
class Type extends Enum{
public static final Type A;
public static final Type B;
public static final Type C;
public static final Type D;
}

在枚举中我们明确了构造方法限制为私有,在我们访问枚举实例时会执行构造方法,同时每个枚举实例都是 static final 类型的,也就表明只能被实例化一次。在调用构造方法时,我们的单例被实例化。
也就是说,因为 enum 中的实例被保证只会被实例化一次,所以我们的 INSTANCE 也被保证实例化一次。

1
2
3
4
5
6
7
8
9
10
11
12
13
public enum TokenProviderSingleton {

INSTANCE;

private TokenProvider mInst;

TokenProviderSingleton() {
mInst = new TokenProvider();
}
public TokenProvider getInst() {
return mInst;
}
}
------ 本文结束 🎉🎉 谢谢观看  ------