跳转到主要内容
返回文章列表

Java 单例模式实现

Java4 分钟阅读 · 1254
#Java#设计模式#单例#线程安全

问题

如何用 Java 实现线程安全的单例?不同写法分别适合什么场景?

回答

核心结论

单例模式的目标只有一个:

  • 一个类只保留一个实例
  • 全局可访问

但“只创建一个对象”不等于“写一个静态变量就完事”,还要考虑:

  • 线程安全
  • 延迟加载
  • 反射破坏
  • 序列化破坏
  • 是否其实应该交给 Spring 容器管理

常见实现方式对比

方式 线程安全 延迟加载 代码复杂度 备注
饿汉式 简单直接
懒汉式 + synchronized 但每次调用都要同步
DCL 双重检查锁 必须配合 volatile
静态内部类 普通 Java 场景很常用
枚举单例 对反射、序列化更稳

1. 饿汉式

public class Singleton {
    private static final Singleton INSTANCE = new Singleton();

    private Singleton() {}

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

特点:

  • 类加载时就创建实例
  • 实现简单,天然线程安全
  • 如果实例很重、但不一定会用到,可能造成提前初始化

2. 懒汉式 + 同步方法

public class Singleton {
    private static Singleton instance;

    private Singleton() {}

    public static synchronized Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}

优点是好理解,缺点是每次获取都进入同步方法,通常不作为首选。

3. 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

new Singleton() 不是一个不可再分的原子动作。简化理解可以拆成:

  1. 分配内存
  2. 初始化对象
  3. 把引用赋给 instance

如果没有 volatile,就可能出现重排序,让其他线程看到“instance 已经不是 null,但对象还没初始化完”的情况。

评价

  • 能用
  • 但写法复杂,容易被写错
  • 现代 Java 项目中通常不是首选答案

4. 静态内部类

public class Singleton {
    private Singleton() {}

    private static class Holder {
        private static final Singleton INSTANCE = new Singleton();
    }

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

这是很常见、也很优雅的实现方式:

  • 线程安全由类加载机制保证
  • 只有在调用 getInstance() 时才触发内部类加载
  • 没有 DCL 那种额外复杂度

5. 枚举单例

public enum Singleton {
    INSTANCE;

    public void doSomething() {
    }
}

它的优点很明确:

  • 天然线程安全
  • 天然防止反射随意构造
  • 天然规避反序列化再生成新对象的问题

但它更适合 生命周期简单、初始化固定 的单例场景。

推荐口径

场景 更推荐的方案
纯 Java 普通场景 静态内部类
需要额外防反射/反序列化 枚举单例
明确一启动就要用 饿汉式
Spring 项目 优先交给容器管理

反射和序列化为什么会破坏单例

1. 反射

Constructor<Singleton> c = Singleton.class.getDeclaredConstructor();
c.setAccessible(true);
Singleton another = c.newInstance();

如果类没有专门防护,这段代码可能绕过私有构造器限制,创建第二个实例。

2. 序列化

Singleton s1 = Singleton.getInstance();
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("s.obj"));
out.writeObject(s1);

ObjectInputStream in = new ObjectInputStream(new FileInputStream("s.obj"));
Singleton s2 = (Singleton) in.readObject();

如果没有处理 readResolve()s2 可能不是原来的同一个对象。

如何防护

风险 常见防护方式
反射破坏 枚举单例,或在构造器中增加防重复创建校验
序列化破坏 实现 readResolve(),或直接使用枚举单例

Spring 项目里的真实建议

很多业务代码根本不需要手写单例,因为 Spring 默认就把 Bean 作为单例管理:

@Service
public class UserService {
}

真正需要小心的不是“是不是单例”,而是:

单例对象里是否持有共享的可变状态。

错误示例:

@Service
public class UserService {
    private User currentUser;
}

这会让多个请求共享同一份可变数据,风险很高。

一句话总结

手写 Java 单例时,普通场景优先考虑静态内部类;如果还要更稳地防反射和序列化,优先考虑枚举;在 Spring 项目里,通常更应该让容器来管理对象生命周期。

相关问题

  • 为什么 DCL 现在没那么常被推荐? → 因为它可用但复杂,静态内部类通常更简洁、更不容易写错。
  • 枚举单例是不是只能做无状态工具类? → 不是,它也可以有字段和方法,只是更适合初始化简单、生命周期固定的场景。
  • 需要传配置参数时怎么办? → 更常见的做法是工厂、配置对象或依赖注入,而不是为了“单例”强行上 DCL。

技术拓展

判断要不要手写单例的顺序

可以按这个顺序思考:

  1. 能不能交给框架管理?
  2. 这个对象真的需要全局唯一吗?
  3. 是否存在共享可变状态风险?
  4. 是否要防反射和反序列化?

很多时候,真正的最佳实践不是“把单例写得更花”,而是“尽量少手写单例”。

Learning Note

本文为个人学习记录,主要来自与 AI 对话后的知识整理与实践总结,仅供个人学习参考。