单例模式是最简单也最常考的设计模式,但要写出线程安全且高效的实现并不容易。这篇文章梳理五种主流写法,分析各自的优缺点。
为什么需要单例
某些对象在整个应用中只需要一个实例,比如配置管理器、数据库连接池、线程池等。单例模式确保一个类只有一个实例,并提供全局访问点。
方式一:饿汉式
public class Singleton {
private static final Singleton INSTANCE = new Singleton();
private Singleton() {}
public static Singleton getInstance() {
return INSTANCE;
}
}
类加载时就创建实例,线程安全,实现简单。缺点是无论是否使用都会创建对象,如果初始化开销大会浪费资源。实际开发中,大多数场景下这个"缺点"并不是问题,因为单例类通常都会被用到。
方式二:懒汉式(线程不安全)
public class Singleton {
private static Singleton instance;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
延迟加载,首次调用时才创建。但多线程环境下,两个线程可能同时通过 instance == null 判断,导致创建多个实例。不要在多线程环境中使用这种写法。
方式三:双重检查锁(DCL)
public class Singleton {
private static volatile Singleton instance;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) { // 第一次检查,避免不必要的同步
synchronized (Singleton.class) {
if (instance == null) { // 第二次检查,防止重复创建
instance = new Singleton();
}
}
}
return instance;
}
}
volatile 关键字不可省略。没有它,instance = new Singleton() 可能发生指令重排序:先给 instance 赋引用地址,再执行构造函数。此时另一个线程读到的 instance 不为 null,但对象尚未初始化完成,会产生诡异的错误。
这种写法兼顾了延迟加载和线程安全,性能也不错——只有第一次创建时会进入同步块。
方式四:静态内部类
public class Singleton {
private Singleton() {}
private static class Holder {
private static final Singleton INSTANCE = new Singleton();
}
public static Singleton getInstance() {
return Holder.INSTANCE;
}
}
利用 JVM 类加载机制保证线程安全:内部类 Holder 在第一次被引用时才加载,加载过程由 JVM 保证线程安全。同时实现了延迟加载——只有调用 getInstance() 时才会触发 Holder 的加载。
这是公认最优雅的写法,既线程安全又延迟加载,没有同步开销。
方式五:枚举
public enum Singleton {
INSTANCE;
public void doSomething() {
System.out.println("枚举单例");
}
}
// 使用
// Singleton.INSTANCE.doSomething();
《Effective Java》推荐的方式。枚举天然是线程安全的单例,而且能防止反序列化和反射攻击创建新实例——这是前面四种方式都无法做到的。缺点是写法不够直观,继承 enum 后不能再继承其他类。
各方式对比
| 方式 | 线程安全 | 延迟加载 | 防反射/反序列化 | 推荐度 |
|---|---|---|---|---|
| 饿汉式 | 是 | 否 | 否 | 适合大多数场景 |
| 懒汉式 | 否 | 是 | 否 | 不推荐 |
| 双重检查锁 | 是 | 是 | 否 | 推荐 |
| 静态内部类 | 是 | 是 | 否 | 推荐 |
| 枚举 | 是 | 否 | 是 | 最安全 |
没有绝对最好的方式,根据场景选择:普通业务用饿汉式或静态内部类即可,对安全性有极高要求的场景用枚举。