跳到主要内容

单例模式

问题

什么是单例模式?Java 中有几种实现方式?如何保证线程安全?

答案

核心思想

单例模式确保一个类只有一个实例,并提供全局访问点

实现方式对比

实现方式线程安全懒加载防反射防序列化推荐度
饿汉式★★★
懒汉式(synchronized)★★
双重检查锁(DCL)★★★★
静态内部类★★★★
枚举★★★★★

饿汉式

类加载时就创建实例,天然线程安全,但不能懒加载。

饿汉式单例
public class Singleton {
// 类加载时立即初始化
private static final Singleton INSTANCE = new Singleton();

private Singleton() {}

public static Singleton getInstance() {
return INSTANCE;
}
}

双重检查锁(DCL)

兼顾线程安全和性能,是最常见的面试答案。

DCL 单例
public class Singleton {
// volatile 防止指令重排序
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() 不是原子操作,JVM 可能重排序为:

  1. 分配内存
  2. 将引用指向内存(此时 instance != null,但还没初始化)
  3. 执行构造方法

volatile 禁止 2 和 3 重排序,确保其他线程拿到的是完全初始化的对象。详见 volatile 关键字

静态内部类

利用类加载机制保证线程安全,同时实现懒加载。

静态内部类单例
public class Singleton {
private Singleton() {}

// 内部类在第一次被使用时才加载
private static class Holder {
private static final Singleton INSTANCE = new Singleton();
}

public static Singleton getInstance() {
return Holder.INSTANCE;
}
}

枚举(推荐)

《Effective Java》推荐的方式,天然防止反射和序列化破坏。

枚举单例
public enum Singleton {
INSTANCE;

private final Connection connection;

Singleton() {
// 初始化资源
connection = createConnection();
}

public Connection getConnection() {
return connection;
}
}

// 使用
Singleton.INSTANCE.getConnection();

Spring 中的单例

Spring Bean 默认是单例的(@Scope("singleton")),但它是 Spring 容器管理的单例,不是 JVM 层面的单例——同一个类可以在不同的 Spring 容器中有不同实例。

Spring 单例 Bean
@Service // 默认单例
public class UserService {
// Spring IoC 容器保证只创建一个实例
}

常见面试问题

Q1: 为什么 DCL 中需要两次 if 判断?

答案

  • 第一次 if:快速路径,已经创建的情况下直接返回,不用进入 synchronized 加锁
  • 第二次 if:线程 A 和 B 同时通过第一次 if,A 先拿到锁创建了实例,B 拿到锁后需要再检查一次避免重复创建

Q2: 如何破坏单例?如何防御?

答案

两种破坏方式:

  1. 反射:通过 Constructor.newInstance() 调用私有构造器
  2. 序列化:反序列化会创建新对象

防御方式:

// 防反射:在构造器中检查
private Singleton() {
if (INSTANCE != null) {
throw new RuntimeException("不允许反射创建");
}
}

// 防序列化:添加 readResolve 方法
private Object readResolve() {
return INSTANCE;
}

枚举单例天然免疫这两种攻击。

Q3: 单例模式在 Java 中有哪些典型应用?

答案

  • Runtime.getRuntime():饿汉式
  • Spring IoC 容器中的 Bean:容器级单例
  • 数据库连接池、线程池
  • 日志对象(如 LoggerFactory

Q4: 单例模式有什么缺点?

答案

  1. 难以测试:全局状态不利于单元测试的隔离
  2. 隐藏依赖:通过 getInstance() 获取,依赖关系不透明
  3. 违反单一职责:既管理实例生命周期,又承担业务逻辑

在 Spring 项目中,优先使用 Spring IoC 管理的单例 Bean,而不是自己实现单例模式。

相关链接