Skip to content
设计模式与结构抽象 · 第 3 篇 / 共 3 篇
领域编程与框架
专题架构与设计专题
当前序列设计模式与结构抽象
阅读位置第 3 篇 / 共 3 篇当前专题第 1 个序列 / 共 3 个序列

单例模式怎么写更稳:饿汉式、双重检查锁、静态内部类、枚举

单例模式看起来很简单,但它其实是很多人第一次真正接触“线程安全实现细节”的地方。

因为你不是只要“全局只有一个对象”,而是要在并发环境下仍然只创建出一个对象。

先说结论

在 Java 里,更常见也更稳的几个选择是:

  • 简单直接:饿汉式
  • 延迟加载且实现稳妥:静态内部类
  • 最简洁也最抗反射/反序列化:枚举单例
  • 双重检查锁可以用,但实现要非常规范

如果不是面试题,工程里优先考虑:

  • 静态内部类
  • 枚举单例

一、单例到底解决什么问题

单例适合那些天然只希望有一个实例的对象,例如:

  • 配置中心客户端
  • 连接池管理器
  • 某些全局资源调度器
  • 无状态工具型服务的统一入口

它的目标通常有两个:

  • 保证全局唯一
  • 避免重复创建重对象

二、饿汉式:最简单,也最直接

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

    private Singleton() {
    }

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

优点

  • 线程安全
  • 写法简单

缺点

  • 类加载时就创建对象
  • 如果这个对象很重、又不一定会用到,就会提前占资源

如果对象本身不重,而且一定会被用到,这种方式其实非常实用。

三、懒汉式:延迟加载,但要小心线程安全

很多人最早写单例会写成这样:

java
public class Singleton {
    private static Singleton instance;

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

这在单线程里没问题,但多线程下可能会创建出多个实例,所以不能直接用于并发环境。

四、双重检查锁:可以用,但别漏掉 volatile

java
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

因为对象创建不是一个原子步骤,底层可能经历:

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

如果发生指令重排,其他线程可能拿到一个“引用已经不为空,但对象还没完全初始化”的实例。

volatile 的作用,就是约束这类重排风险。

这种写法的特点

  • 支持延迟加载
  • 线程安全
  • 写法比饿汉式复杂

所以它能用,但不是第一优先推荐。

五、静态内部类:延迟加载和实现简洁的平衡点

java
public class Singleton {
    private Singleton() {
    }

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

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

为什么这种方式很实用

  • 外部类加载时,不会立刻初始化内部类
  • 第一次调用 getInstance() 时,才触发内部类加载
  • 类加载过程天然保证线程安全

所以它同时兼顾了:

  • 延迟加载
  • 线程安全
  • 实现简洁

这也是工程里很常见的一种写法。

六、枚举单例:最简洁,也最抗打

java
public enum Singleton {
    INSTANCE;
}

这在很多场景下是最稳的单例写法,因为它天然防住了两个容易被忽略的问题:

  • 反序列化破坏单例
  • 反射破坏单例

如果你的单例不需要复杂继承层次,枚举方式非常值得优先考虑。

七、工程里怎么选

1. 对象一定会被用到,而且不重

用饿汉式,简单直接。

2. 希望延迟加载,又想保持代码简洁

用静态内部类。

3. 希望实现最稳,顺便规避反序列化和反射问题

用枚举单例。

4. 已经有固定风格,或者面试场景强调并发细节

可以写双重检查锁,但别漏掉 volatile

八、单例模式最容易被误用的地方

1. 把太多可变状态塞进单例

单例只保证“对象唯一”,不保证“内部状态天然线程安全”。

2. 把单例当成全局变量仓库

这样很容易让依赖关系变隐式,代码反而更难维护。

3. 看到单例就以为一定更省资源

如果对象本身很轻,或者生命周期受容器管理,单例未必是重点优化方向。

一句话总结

单例模式最关键的不是记住几种写法,而是搞清楚线程安全和初始化时机。

如果只求实用,Java 工程里优先考虑静态内部类或枚举单例,通常已经足够稳。

延伸阅读相关文章优先当前专题,再补跨专题关联。
同一序列 · 回看前文会更完整工厂模式怎么选适合把设计模式、对象创建和结构抽象能力放在一组里系统理解。架构与设计专题 · 设计模式与结构抽象同一序列 · 回看前文会更完整设计模式总览适合把设计模式、对象创建和结构抽象能力放在一组里系统理解。架构与设计专题 · 设计模式与结构抽象同专题其他序列 · 分布式系统设计基线接口幂等到底应该怎么设计适合把幂等、限流、分布式 ID 和一致性模型放在同一条架构主线上看。架构与设计专题 · 分布式系统设计基线同专题其他序列 · 分布式系统取舍与边界聚合根和事务边界怎么划分适合把 DDD、Saga、TCC 和 BFF、网关边界放在一起看。架构与设计专题 · 分布式系统取舍与边界跨专题关联 · 同场景:基础学习sync.Once 和单例初始化的常见坑适合把 goroutine 生命周期、context 边界、接口抽象和优雅停机放回 Go 服务实践里思考。Go 专题 · Go 并发模型与服务工程化跨专题关联 · 同场景:基础学习@Conditional 系列注解怎么配合使用适合把自动装配、条件装配、Profile 和循环依赖放回 Spring Boot 启动过程里理解。Spring 专题 · Spring Boot 启动、装配与配置
继续阅读设计模式与结构抽象当前序列第 3 篇 / 共 3 篇当前专题第 1 个序列 / 共 3 个序列
往前看
上一篇工厂模式怎么选回到当前序列上一章上一序列Spring Boot 与事务实践从第 1 篇开始:Spring Boot 开发笔记
往后看
下一序列分布式系统设计基线从第 1 篇开始:接口幂等到底应该怎么设计

把零散经验整理成可查、可复用、可持续更新的企业级知识门户