基本概念
顾名思义,单例模式,是指一个类只能生成一个唯一的对象。单例模式主要用于一些无状态的组件或者本质上唯一的组件。
实现单例模式,有比较多的方法,各种不同方法,各有优劣和适用场景。
一个单例模式的实现,需要按需实现以下功能:
- 如果这个单例会被多个线程访问,则需要实现并发安全;
- 考虑防止单例特性被反射方式破坏;
- 考虑方式单例特性被序列化方式破坏。
几种常见单例模式
饿汉式单例模式
基础实现
饿汉式单例模式,主要是指构造方法私有,实例对象在类加载的时候就初始化,用共有方法提供对象访问点。代码如下所示:
public class HungrySingleton {
private static final HungrySingleton hungrySingleton = new HungrySingleton();
private HungrySingleton(){};
public static HungrySingleton getInstance() {
return hungrySingleton;
}
}
这种单例模式优点是线程安全的,而且是无锁的,缺点有两个,一是每个实例不论是否调用,都被初始化,如果单例对象比较多,可能会有性能问题;二是私有化构造器不是绝对安全的,反射方式调用构造器是可以创建多个对象的。
针对缺点一,后文中的懒汉式单例模式是一种解决方案。对于缺点二,我们可以在构造器中做出限制,一旦多次调用构造器,就抛出异常。
防止反射破坏单例特性
为了防止单例特性被反射破坏,我们在构造器中加入限制,修改后的饿汉式单例模式如下:
public class HungrySingleton {
private static final HungrySingleton hungrySingleton = new HungrySingleton();
private HungrySingleton(){
if (hungrySingleton != null) {
throw new RuntimeException("Could not construct more than one object of a Singleton.");
}
};
public static HungrySingleton getInstance() {
return hungrySingleton;
}
}
我们用下面的测试代码测试前面的两个单例模式代码,
import java.lang.reflect.Constructor;
public class HungrySingletonTest {
public static void main(String[] args) {
try {
Class<?> cla = HungrySingleton.class;
Constructor<?> c = cla.getDeclaredConstructor((Class<?>) null);
c.setAccessible(true);
Object o1 = c.newInstance();
Object o2 = c.newInstance();
System.out.println(o1 == o2);
} catch (Exception e) {
e.printStackTrace();
}
}
}
没有限制的饿汉单例模式,会遭到破坏,产生两个实例,输出 false
,而后一个对构造函数做了限制的代码会按照预期抛出运行时异常,组织单例模式被破坏。
防止序列化破坏单例特性
下面是实现序列化接口的单例模式:
public class HungrySingleton implements Serializable {
private static final HungrySingleton hungrySingleton = new HungrySingleton();
private HungrySingleton(){
if (hungrySingleton != null) {
throw new RuntimeException("Could not construct more than one object of a Singleton.");
}
};
public static HungrySingleton getInstance() {
return hungrySingleton;
}
}
使用以下代码测试:
public class HungrySingletonTest {
public static void main(String[] args) {
HungrySingleton singleton1 = null;
HungrySingleton singleton2 = HungrySingleton.getInstance();
try {
FileOutputStream fos = new FileOutputStream("Singleton.obj");
ObjectOutputStream oos = new ObjectOutputStream(fos);
oos.writeObject(singleton2);
oos.flush();
oos.close();
FileInputStream fis = new FileInputStream("Singleton.obj");
ObjectInputStream ois = new ObjectInputStream(fis);
singleton1 = (HungrySingleton) ois.readObject();
ois.close();
System.out.println(singleton1);
System.out.println(singleton2);
System.out.println(singleton1 == singleton2);
} catch (Exception e) {
e.printStackTrace();
}
}
}
得到的输出:
com.coolxxy.HungrySingleton@4590c9c3
com.coolxxy.HungrySingleton@737996a0
false
说明序列化破坏了单例模式,要解决这个问题,只需要实现 readResolve 方法:
public class HungrySingleton implements Serializable {
private static final HungrySingleton hungrySingleton = new HungrySingleton();
private HungrySingleton(){
if (hungrySingleton != null) {
throw new RuntimeException("Could not construct more than one object of a Singleton.");
}
};
private Object readResolve() {
return hungrySingleton;
}
public static HungrySingleton getInstance() {
return hungrySingleton;
}
}
再进行同样的测试,得到的输出如下:
com.coolxxy.HungrySingleton@737996a0
com.coolxxy.HungrySingleton@737996a0
true
至于为何添加这个方法的实现就可以解决序列化破坏单例问题,一句话粗略解释,就是如果实现了 readResolve 这个方法,反序列化时 readObject 方法会最终返回 readResolve 的执行结果,也就是我们指定的那个单例对象。这个点的详细解释,可以看这个链接 掘金:单例、序列化和readResolve()方法。
懒汉式单例模式
懒汉单例模式是指,延迟单例对象的初始化,直到需要这个对象的时候,再初始化它,按照懒汉单例模式的语义,写出以下
代码:
public class LazySimpleSingleton {
private LazySimpleSingleton(){};
private static LazySimpleSingleton lazySimpleSingleton = null;
public static LazySimpleSingleton getInstance() {
if (lazySimpleSingleton == null) {
lazySimpleSingleton = new LazySimpleSingleton();
}
return lazySimpleSingleton;
}
}
以上的代码中,当 getInstance
被调用时,检测是否被初始化,从而把初始化延迟到调用期,而不是类加载时。
但是以上代码不是并发安全的,因为 getInstance
函数里面的代码不是原子的,可能有多个线程同时检测到符合 if
语句的判断条件,从而各自进行初始化。针对此问题,最简单的修改方式是把 getInstance
方法加上 synchronized
关键字,将其变成同步方法,这样就解决了线程安全问题。
但是直接锁定 getInstance
方法,会引入性能问题,因为可能同时有多个线程访问单例对象,那么会造成大量线程阻塞,所以我们可以改成所谓的双重检查锁定单例代码,如下所示:
public class LazySimpleSingleton {
private LazySimpleSingleton(){};
private volatile static LazySimpleSingleton lazySimpleSingleton = null;
public static LazySimpleSingleton getInstance() {
if (lazySimpleSingleton == null) {
synchronized (LazySimpleSingleton.class) {
if(lazySimpleSingleton == null) {
lazySimpleSingleton = new LazySimpleSingleton();
}
}
}
return lazySimpleSingleton;
}
}
这里的优化思路,主要是缩小加锁区域到初始化单例对象,而不是锁定整个 getInstance 方法,这样就可以减少因为获取不到锁而导致的阻塞。但是如果只是做这样的先检查再锁定是不对的,因为存在指令重排序,可能会发生,第一个访问单例对象的线程,执行 new LazySimpleSingleton()
时,初始化内存区域被重排到了设置对象引用之后,那么后面的其他线程会检测到单例对象不为 null 而直接去获取单例对象,从而获取到初始化未完成的对象,出现严重错误。因此把 lazySimpleSingleton 声明为 volatile 禁止指令重排是必须的。
实现懒汉式单例模式还有一种思路是基于静态内部类实现,代码如下:
public class HungrySingleton implements Serializable {
private static class InstanceHolder {
public static HungrySingleton hungrySingleton = new HungrySingleton();
}
public static HungrySingleton getInstance() {
return InstanceHolder.hungrySingleton;
}
}
这种思路首先用一个静态内部类持有单例对象,而静态内部类会在使用的时候,也就是调用外部类的 getInstance 时候再初始化,从而达到延迟初始化的目标。而单例的实现是依靠 jvm 的类加载机制,jvm 类加载时候,会用 Class 对象的初始化锁保证类被唯一加载,从而保证了单例特性。
针对懒汉单例模式解决序列化和反射破坏的思路是基本一致的,这里不在详细展开。
另外一点是,延迟初始化减少了初始化类的开销,但是初始化迟早是要做的,延迟初始化意味着首次访问时的开销必然增大。除非明确需要延迟初始化,否则没必要做这些额外工作。
枚举单例模式
利用枚举的特性,可以使用以下代码构建一个符合单例模式要求的 Enum:
public enum EnumSingleton {
INSTANCE;
public static EnumSingleton getInstance() {
return INSTANCE;
}
}
这种单例模式的代码非常优雅,天然线程安全,而且 Enum 的 JDK 实现,天然保障了即使刻意使用反射和序列化,也无法破坏单例特性。再没有其他限制的场景下,这是一种最简单优雅的单例模式,但是如果单例必须扩展一个超类来实现的话,这种方式不是很适合。(Enum 单例模式也是 《effective java》一书中推荐的方式)
总结
本文介绍了多种实现单例模式的方式,总结一个简单的单例模式实现思路:
- 如果没有额外限制,就用 Enum 实现单例;
- 否则,如果没有特殊要求,就用饿汉式,也就是不要延迟初始化;
- 否则,考虑延迟懒汉式,其中 volatile 加双重检测锁定比较适合实例字段需要延迟初始化场景,静态内部类适合对静态字段延迟初始化;
- 如果使用了 Enum 之外的方式,需要考虑序列化破坏和反射破坏。