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 篇开始:接口幂等到底应该怎么设计

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