单例模式

单例模式

定义:单例对象的类必须保证只有一个实例存在。

场景:希望在整个系统中只能出现某个类的一个实例。

分类:

对单例的实现可以分为两大类——懒汉式和饿汉式,他们的区别在于:

  • 懒汉式:指全局的单例实例在第一次被使用时构建。
  • 饿汉式:指全局的单例实例在类装载时构建。

日常我们使用的较多的应该是懒汉式的单例,因为按需加载才能做到资源的最大化利用。

单例模式的实现

懒汉式单例

Version 1.0

1
2
3
4
5
6
7
8
9
10
public class Single {
private static Single instance;
private Single(){}
public static Single getInstance() {
if (instance == null) {
instance = new Single();
}
return instance;
}
}

上述代码所做的事情就是每次获取instance之前先进行判断,如果instance为空就new一个出来,否则就直接返回已存在的instance。

显然,这种做法是在单线程的条件下才能正常工作的单例模式。如果工作在多线程环境,多个线程有可能同时进入 if (instance == null){…} ,那么就会创建不止一个类的实例了。

Version 2.0

既然Version1.0在多线程环境下无法正常工作,那么最直接的解决办法,用synchronized互斥锁来达到线程安全的运行环境。

1
2
3
4
5
6
7
8
9
10
public class Single {
private static Single instance;
private Single(){}
public synchronized static Single getInstance() {
if (instance == null) {
instance = new Single();
}
return instance;
}
}

相比于Version1.0,只是在getInstance函数加上synchronized修饰符。但是,我们知道synchronized是互斥锁,就意味着对阻塞线程,很大影响了程序的访问效率。

Version 3.0

为了解决synchronized互斥锁带来的效率上的影响,我们引进了双重检查的机制,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Single {
private static Single instance;
private Single() {}
public static Single getInstance() {
//线程A在此处
if (instance == null) {
synchronized (Single.class) {
if (instance == null) {
instance = new Single();//线程B在此处
}
}
}
return instance;
}
}

上述代码,看起来很美好,但是存在隐患。

首先,我们要知道instance = new Single(); 这个语句在JVM中,实际执行的原子语句是哪些?

  1. 给Single分配内存
  2. 调用Single的构造函数完成初始化,返回类实例
  3. 将Single对象指向之前分配的完成初始化的内存

但是,编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序。那么,上述1-2-3的指令执行顺序有可能就变成了1-3-2

我们假设一种情况,线程A和线程B同时访问getInstance()方法。线程A和线程B分别运行在上述代码注释所在地。
假设,线程B的new Single()操作被指令重排了的,执行到了1-3:已经将分配的内存指向了instance对象,但是并未执行初始化操作,所以这个时候instance对象不等于null,但是没有被初始化。然后,线程B让出CPU,线程A开始执行,判断if (instance == null),这时我们可以知道instance是不等于null的,于是线程A就直接返回这个instance对象,但是这个对象是线程B没有初始化的对象!

问题的关键在于:线程B对instance的写操作没有完成,线程A就执行了读操作。

Version 4.0

继续改进我们的懒汉式单例实现代码:针对Version 3.0的双锁检查可能遇到的指令重排带来的线程不安全,Java语言提供了volatile关键词,来帮助我们禁止指令重排。更多关于Java内存模型的内容参考这篇博客

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Single {
private static volatile Single instance;
private Single() {}
public static Single getInstance() {
if (instance == null) {
synchronized (Single.class) {
if (instance == null) {
instance = new Single();
}
}
}
return instance;
}
}

以上就是线程安全的懒汉式单例的最终的写法。

饿汉式单例

饿汉式单例:指全局的单例实例在类装载时构建的实现方式

静态内部类实现

1
2
3
4
5
6
7
8
9
public class Singleton {
private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}
private Singleton (){}
public static final Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
}

SingletonHolder是一个内部类,只在外部类的Singleton的getInstance()中被使用,所以它被加载的时机也就是在getInstance()方法第一次被调用的时候。在SingletonHolder初始化的时候会由ClassLoader来保证同步,使INSTANCE是一个线程安全的单例。

枚举实现

1
2
3
4
5
6
7
8
9
public enum SingleInstance {
INSTANCE;
public void fun1() {
// do something
}
}
// 使用
SingleInstance.INSTANCE.fun1();

enum的语法结构虽然和class的语法不一样,但是经过编译器编译之后产生的是一个class文件。该class文件经过反编译可以看到实际上是生成了一个类,该类继承了java.lang.Enum。所以,enum是由class实现的,也就是说,enum可以实现很多class的内容,包括可以有类成员变量和类方法函数,这也是我们可以用enum作为一个类来实现单例的基础。

使用枚举其实和使用静态类内部加载方法原理类似。java.lang.Enum是Java提供给编译器的一个用于继承的类。枚举量的实现其实是public static final类型的未初始化变量。如果枚举量有伴随参数并且手动添加了构造器,那么将会解析成一个静态的代码块在类加载时对变量进行初始化。

另外,由于enum是通过继承了Enum类实现的,enum结构不能够作为子类继承其他类,但是可以用来实现接口。此外,enum类也不能够被继承,在反编译中,我们会发现该类是final的。enum有且仅有private的构造器,防止外部的额外构造,这恰好和单例模式吻合,也为保证单例性做了一个铺垫。

最后对于序列化和反序列化,因为每一个枚举类型和枚举变量在JVM中都是唯一的,即Java在序列化和反序列化枚举时做了特殊的规定,枚举的writeObject、readObject、readObjectNoData、writeReplace和readResolve等方法是被编译器禁用的,因此也不存在实现序列化接口后调用readObject会破坏单例的问题。

从现在来看,枚举已经成为了单例模式的最佳实践。

0%