Java 设计模式之单例模式

单例模式应该是大家最熟悉的设计模式,但是单例模式有好几种实现方式,下面就分析各种实现方式的优缺点。

概念

单例模式,即单例类只能有一个实例,并且对外提供一个全局访问入口。

下面依次介绍几种实现方式,关键在于如何创建唯一的实例。

饿汉式

饿汉式,是指类装载时就已经创建了实例,也是最简单的实现方式。

1
2
3
4
5
6
7
public class Singleton {
private static final Singleton sInstance = new Singleton();
private Singleton() {}
public static Singleton getInstance() {
return sInstance;
}
}

饿汉式的实现方式,因其在类装载的时候就创建了实例,所以天生就是线程安全的。但是还有两个问题:1)如果构造方法中有耗时操作的话,会导致这个类的加载比较慢 2)饿汉式一开始就创建实例,但是并没有调用,会造成资源浪费。

懒汉式

懒汉式,是指在第一次调用的时候才去创建实例。

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

上面的实现方式有个很大问题,是非线程安全的。多个线程调用getInstance时,可能会创建多个实例的可能。试想一下,A 线程执行到sInstance = new Singleton()这句,还没赋值给 sInstance 时 B 线程调用getInstance,因为 sInstance 为空,又会创建一个实例。

synchronized 修饰方法

对于懒汉式的线程安全问题,最容易想到就是在getInstance方法上加synchronized,保证同一时间只能一个线程调用。

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

但是每次调用都同步,虽然保证了线程安全,却影响了性能.

双重检查锁定

双重检查锁定,是只在创建实例的代码块加锁,但是仅适用于 Java 5.0 以上。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Singleton {
private static volatile Singleton sInstance;
private Singleton() {}
public static Singleton getInstance() {
if(null == sInstance) { //第一次检查
synchronized(Singleton.class) {
if(null == sInstance) { //第二次检查
sInstance = new Singleton();
}
}
}
return sInstance;
}
}

双重检查锁定中同步代码块,保证同一时间只有一个线程可以进入,而第二个线程进入的时候检查不为空后就不会再创建实例了。

其中如果没有volatile修饰静态变量的话,代码可能会出现问题。由于 Java 编译器和 JIT 的优化的原因,指令的顺序可能被重排。这样就可能会出现这样的情况:线程 A。在没有构造完成后就赋值给 sInstance,这样线程 B 调用时判断不为空,这时使用未构造完成的实例会出现问题。volatile是轻量级的 synchronized,保证了可见性,即一个线程修改了变量后,另一个线程能读到修改后的变量。而且在 Java 5.0 之后还可以保证顺序性",它还可以禁止指令重排序优化,可以保证线程 B 读之前线程 A 已经构造完成实例。所以双重检查锁定只有在 Java 5 之后才有效,更多 volatile 关键字解析请看 线程安全之 volatile 关键字

静态内部类(推荐实现方式)

延迟加载还可以通过静态内部类来实现,静态内部类只有在第一次使用的时候才会被装载。

1
2
3
4
5
6
7
8
9
public class Singleton {
private static class Holder {
private static Singleton sInstance = new Singleton();
}
private Singleton() {}
public static getInstace() {
return Holder.sInstance;
}
}

类的静态初始化在类被装载的时候触发,而创建实例化的过程由JVM保证线程安全。所以这种方式是简单实用的,推荐大家使用。

枚举

“Effective Java” 书中还提到了另一种实现方式,使用枚举来保证单例

1
2
3
4
5
public enum Singleton {
INSTANCE;
public void methodXXX() {...}
}

枚举可以保证线程安全和序列化多个实例,还可以防止反射创建实例,但是这种实现方式不能延迟加载。

唯一实例么

上面几种实现方式,除了枚举,其他都可以通过反射和序列化创建多个实例。而且使用多个类加载器,即多进程下也能创建多个实例,当然这种情况比较少见。

一般情况,不考虑极端情况下,推荐使用静态内部类实现单例就可以了。