概述
单例模式,是设计模式中最常见的模式之一,它是一种创建对象模式,用于产生一个对象的具体实例,可以确保系统中一个类只会产生一个实例。
优缺点
优点
- 对于频繁使用的对象,可以省去 new 操作花费的时间,尤其对那些重量级对象而言,削减了一笔非常客观的系统开销。
- 由于 new 操作的次数减少,因而对系统内存的使用频率也会降低,从而减轻 GC 压力,缩短 GC 停顿时间。
缺点
- 单例模式一般没有接口,扩展困难。如果要扩展,则除了修改原来的代码,没有第二种途径,违背开闭原则。
- 在并发测试中,单例模式不利于代码调试。在调试过程中,如果单例中的代码没有执行完,也不能模拟生成一个新的对象。
- 单例模式的功能代码通常写在一个类中,如果功能设计不合理,则很容易违背单一职责原则。
场景
- Spring 中创建的 Bean 实例默认都是单例。
- 数据库连接池的设计与实现。
- 多线程的线程池设计与实现。
核心结构
单例模式的核心在于通过一个接口返回唯一的对象实例。
常见写法
1.饿汉模式
代码语言:javascript复制public class Singleton {
private Singleton() {
System.out.println("create Singleton");
}
private static Singleton instance = new Singleton();
public static Singleton getInstance(){
return instance;
}
}
饿汉模式单例的实现方式简单,在 JVM 对类加载的时候,单例对象就会被创建,因此线程安全。由于获取实例的静态方法没有使用同步方法,调用效率高。但如果该实例从始至终都没被使用过,则会造成内存浪费。
2.懒汉模式
代码语言:javascript复制public class LazySingleton {
private LazySingleton() {
System.out.println("create Singleton");
}
private static LazySingleton instance = null;
public static synchronized LazySingleton getInstance() {
if (instance == null) {
instance = new LazySingleton();
}
return instance;
}
}
懒汉模式单例是对静态成员变量 instance 赋予初始值 null,确保系统启动时没有额外的负载。在第一次使用的时候才进行初始化,达到了懒加载的效果。由于获取实例的静态方法用 synchronized 关键字修饰,所以线程安全。但是由于每次获取实例都要进行同步加锁,因此效率较低。
3.双重检测机制(DCL)
代码语言:javascript复制public class DCLSingleton {
private DCLSingleton() {
System.out.println("create Singleton");
}
private static volatile DCLSingleton instance = null;
public static DCLSingleton getInstance() {
if (instance == null) {
synchronized (DCLSingleton.class) {
if(instance == null){
instance = new DCLSingleton();
}
}
}
return instance;
}
}
双重检测机制(双重检查加锁)是在第一次使用的时候才进行初始化,达到了懒加载的效果。在进行初始化的时候会进行同步加锁,因此线程安全。并且只有第一次进行初始化才进行同步,因此不会有效率方面的问题。
CPU 内部会在保证不影响最终结果的前提下对指令进行重新排序(不影响最终结果只是针对单线程,切记),指令重排的主要目的是为了提高效率。
正常情况按顺序执行,双重检测机制是没有问题。如下:
代码语言:javascript复制memory = allocate(); // 1.分配对象的内存空间
ctorInstance(memory); // 2.初始化对象
instance = memory; // 3.设置 instance 指向刚才分配的内存地址
指令重排需后:
代码语言:javascript复制memory = allocate(); // 1.分配对象的内存空间
instance = memory; // 3.设置 instance 指向刚才分配的内存地址
ctorInstance(memory); // 2.初始化对象
如果线程 A 执行完 1 和 3,instance 对象还未完成初始化,但是已经不再指向 null。此时线程 B 抢占到 CPU 资源,执行第12 行的检测结果为 false,则执行第19行,从而返回一个还未初始化完成的 instance 对象,从而出导致问题出现。
使用 volatile 关键字修饰 instance 对象可以禁止指令重排序。
4.静态内部类
代码语言:javascript复制public class StaticInnerHolderSingleton {
private StaticInnerHolderSingleton(){
System.out.println("create Singleton");
}
private static class InnerHolder{
private static StaticInnerHolderSingleton instance = new StaticInnerHolderSingleton();
}
public static StaticInnerHolderSingleton getInstance(){
return InnerHolder.instance;
}
}
当 StaticInnerHolderSingleton 被加载时,内部类 InnerHolder 并不会被初始化,只有在 getInstance() 方法被调用时,才会加载 InnerHolder,从而初始化 instance,做到了延迟加载。
StaticInnerHolderSingleton 实例的创建在 Java 编译时期收集在 () 中,该方法又是同步方法,可以保证内存的可见性、JVM指令的顺序性以及原子性。
5.枚举
代码语言:javascript复制public enum EnumSingleton {
INSTANCE;
EnumSingleton(){
System.out.println("create Singleton");
}
// 调用getInstance方法,事实上获得Holder的instance静态属性
public static EnumSingleton getInstance(){
return INSTANCE;
}
枚举类型不允许被继承,同时是线程安全且只能被实例化一次,但是枚举类型不能够懒加载,对 EnumSingleton 主动使用,如用其中的静态方法 INSTANCE 会立即实例化。
通过 Java 反射机制或序列化和反序列化可能会破坏单例,但枚举模式的单例天然不存在这个问题。
单例破坏问题
通过 Java 反射机制,强行调用单例类的私有构造函数可以生成多个单例示例,这种情况相对极端,代码中也不会去如此实现。
对于序列化和反序列化,可以通过私有方法 readResolve() 解决这个问题,代码如下:
代码语言:javascript复制public class SerializableSingleton implements Serializable {
private SerializableSingleton() {
System.out.println("create Singleton");
}
private static SerializableSingleton instance = new SerializableSingleton();
public static SerializableSingleton getInstance(){
return instance;
}
private Object readResolve(){
return instance;
}
}
测试代码如下,可以自行测试:
代码语言:javascript复制public static void main(String[] args) throws Exception {
SerializableSingleton s1 = null;
SerializableSingleton s = SerializableSingleton.getInstance();
// 先将实例序列化到文件
FileOutputStream fos = new FileOutputStream("SerializableSingleton.txt");
ObjectOutputStream oos = new ObjectOutputStream(fos);
oos.writeObject(s);
oos.flush();;
oos.close();
// 从文件反序列化读出原有的单例类
FileInputStream fis = new FileInputStream("SerializableSingleton.txt");
ObjectInputStream ois = new ObjectInputStream(fis);
s1 = (SerializableSingleton) ois.readObject();
System.out.println(s.equals(s1));
}
事实上,在实现了私有的 readResolve() 方法后,readObject() 方法就已经形同虚设,它直接使用 readResolve() 替换了原本的返回值,从而从形式上构造了单例。
总结
在实际工作中,单例的使用还是比较常见的,在几种实现方式中,双重检测机制、静态内部类、枚举方式都是比较推荐。