Java 单例模式完全指南:七种实现方式详解

单例模式(Singleton)保证一个类在 JVM 中只有一个实例,并提供一个全局访问点。


目录

  1. 为什么需要单例
  2. 懒汉式:双重检查锁定(DCL)
  3. 饿汉式:类加载时创建
  4. 静态内部类(Holder)
  5. 枚举单例
  6. 防反射 + 防序列化破坏
  7. ThreadLocal 单例
  8. 容器式单例
  9. 七种方式对比
  10. 选型建议

为什么需要单例

典型使用场景:

  • 配置管理器:全局一份配置,避免重复读取
  • 数据库连接池:昂贵资源只初始化一次
  • 日志工具类:统一输出入口
  • 线程池 / 缓存:控制实例数量,节省内存

单例的核心约束:

要求实现手段
构造器私有private 构造函数,外部无法 new
静态访问点getInstance() 或枚举常量
唯一实例静态变量 / 枚举 / 容器保证

1. 懒汉式:双重检查锁定(DCL)

特点:第一次调用 getInstance() 时才创建实例(懒加载),通过双重 if + synchronized 保证线程安全。

完整代码

package com.zx.generater;

/**
 * 懒汉式 - 双重检查锁定(DCL)
 */
public class OneGenerator {

    // ① 必须用 volatile,防止指令重排导致半初始化对象被读到
    private static volatile OneGenerator instance;

    private OneGenerator() {
    }

    public static OneGenerator getInstance() {
        // ② 第一次检查:已创建则直接返回,避免每次都加锁
        if (instance == null) {
            synchronized (OneGenerator.class) {
                // ③ 第二次检查:防止多个线程同时通过第一次检查后重复创建
                if (instance == null) {
                    instance = new OneGenerator();
                }
            }
        }
        return instance;
    }
}

关键细节

为什么需要 volatile

instance = new OneGenerator() 在字节码层面并非原子操作,大致分为三步:

  1. 分配内存
  2. 初始化对象
  3. 将引用指向内存

JVM 可能对 2、3 步重排序。若线程 A 执行到第 3 步后、第 2 步前被挂起,线程 B 在第一次 if (instance == null) 时可能读到非 null 但未完全初始化的对象。

volatile 禁止这种重排序,并保证多线程间的可见性。

为什么要检查两次?
  • 第一次检查(锁外):实例已存在时,避免每次调用都进入 synchronized,性能更好。
  • 第二次检查(锁内):多个线程同时通过第一次检查时,只有一个能创建实例。
优缺点
优点缺点
懒加载,节省资源代码较复杂
线程安全仍可能被反射破坏(构造器未防护)
高并发下性能较好仍可能被序列化破坏(未实现 readResolve

2. 饿汉式:类加载时创建

特点:类被加载时即创建实例,天然线程安全,实现最简单。

完整代码

package com.zx.generater;

/**
 * 饿汉式 - 类加载时创建实例
 */
public class TwoGenerate {

    // 类加载阶段由类加载器初始化,JVM 保证线程安全
    private static final TwoGenerate INSTANCE = new TwoGenerate();

    private TwoGenerate() {
    }

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

关键细节

类加载时机

static 字段在类初始化阶段<clinit>)赋值。JVM 规范保证:同一个类的 <clinit> 在同一 ClassLoader 下只执行一次,且由 JVM 加锁,因此无需 synchronized

是否算「懒加载」?

不算。只要类被引用(哪怕只调用一个静态方法),就会触发类加载,实例随即创建。若实例很重且不一定用到,会造成浪费。

优缺点
优点缺点
实现极简非懒加载,可能浪费内存
天然线程安全无法传参初始化
无锁,性能好同样可被反射/序列化破坏

3. 静态内部类(Holder)

特点:结合懒加载与线程安全,被广泛认为是最优雅的普通类单例写法

完整代码

package com.zx.generater;

/**
 * 静态内部类(Holder)- 懒加载且线程安全
 */
public class ThreeGenerate {

    private ThreeGenerate() {
    }

    // 内部类持有唯一实例
    private static class Holder {
        private static final ThreeGenerate INSTANCE = new ThreeGenerate();
    }

    public static ThreeGenerate getInstance() {
        // 首次调用时才加载 Holder 类,进而创建 INSTANCE
        return Holder.INSTANCE;
    }
}

关键细节

加载顺序
  1. 外部类 ThreeGenerate 被加载时,不会立即加载 Holder
  2. 第一次调用 getInstance() 时,才触发 Holder 的类加载。
  3. Holder 初始化时创建 INSTANCE,由 JVM 保证线程安全。

这实现了按需懒加载,又避免了 DCL 的 volatile 和双重 if

与饿汉式的区别

饿汉式在外部类加载时就创建实例;Holder 把实例创建推迟到内部类首次被访问时。

优缺点
优点缺点
懒加载 + 线程安全写法略绕(需理解类加载)
无需 synchronized / volatile可被反射/序列化破坏
代码简洁、性能好

4. 枚举单例

特点:Joshua Bloch 在《Effective Java》中推荐的方式,最简洁且最安全

完整代码

package com.zx.generater;

/**
 * 枚举单例 - 天然防反射、防序列化破坏
 */
public enum FourGenerate {
  INSTANCE;

  public void doSomething() {
    // 业务方法示例
  }
}

使用方式

FourGenerate.INSTANCE.doSomething();

关键细节

为什么能防反射?

反射调用 Constructor.newInstance() 时,若目标是枚举,会抛出:

java.lang.IllegalArgumentException: Cannot reflectively create enum objects

JVM 在反射层面禁止实例化枚举。

为什么能防序列化?

普通对象反序列化会调用构造器创建新对象。枚举的反序列化机制不同:反序列化时不会调用构造器,而是根据枚举名查找已有常量,因此始终是同一个 INSTANCE

是否可以懒加载?

枚举实例在枚举类加载时创建,属于饿汉式,无法做到 Holder 那种延迟。

优缺点
优点缺点
代码极简非懒加载
防反射、防序列化无法继承(枚举隐式继承 Enum
序列化安全语义上「枚举即单例」需团队接受

5. 防反射 + 防序列化破坏

特点:在 DCL 基础上,针对反射和序列化两种「破坏单例」的手段做防护。

完整代码

package com.zx.generater;

import java.io.Serializable;

/**
 * 防反射 + 防序列化破坏的单例
 */
public class FiveGenerate implements Serializable {

    private static volatile FiveGenerate instance;

    private FiveGenerate() {
        // 防反射:若 instance 已存在,说明是通过 getInstance 创建的合法实例
        // 反射第二次调用构造器时 instance 非 null,直接抛异常
        if (instance != null) {
            throw new IllegalStateException("禁止通过反射创建单例");
        }
    }

    public static FiveGenerate getInstance() {
        if (instance == null) {
            synchronized (FiveGenerate.class) {
                if (instance == null) {
                    instance = new FiveGenerate();
                }
            }
        }
        return instance;
    }

    /**
     * 防序列化:反序列化时 JVM 会调用 readResolve 替代默认新建对象的行为
     * 始终返回已存在的单例
     */
    private Object readResolve() {
        return getInstance();
    }
}

关键细节

反射攻击原理
Constructor<FiveGenerate> ctor = FiveGenerate.class.getDeclaredConstructor();
ctor.setAccessible(true);
FiveGenerate fake = ctor.newInstance(); // 若不防护,会创建第二个实例

防护逻辑:

  1. 正常流程:getInstance()instance = new FiveGenerate(),此时构造器里 instance == null,通过检查。
  2. 反射流程:再次 new FiveGenerate() 时,instance 已非 null,构造器抛异常。

注意:若攻击者用反射创建实例(在 getInstance 之前),仍可能破坏单例。更严谨的做法是在静态块中设置标志位,或使用枚举单例。

序列化攻击原理
FiveGenerate a = FiveGenerate.getInstance();
// 序列化再反序列化
FiveGenerate b = (FiveGenerate) deserialize(serialize(a));
// 若无 readResolve,a != b

readResolve() 让反序列化返回已有单例,而不是新建对象。

优缺点
优点缺点
懒加载 + 线程安全代码最复杂
防反射(常规路径)反射防护仍有边界情况
防序列化维护成本高

6. ThreadLocal 单例

特点每个线程各自持有一个实例,是「线程内单例」,不是全局单例。

完整代码

package com.zx.generater;

/**
 * ThreadLocal 单例 - 每个线程各自持有一个实例
 */
public class SixGenerate {

    private static final ThreadLocal<SixGenerate> INSTANCE =
            ThreadLocal.withInitial(SixGenerate::new);

    private SixGenerate() {
    }

    public static SixGenerate getInstance() {
        return INSTANCE.get();
    }

    /**
     * 线程池场景下必须调用,否则可能内存泄漏
     */
    public static void remove() {
        INSTANCE.remove();
    }
}

关键细节

与全局单例的区别
维度全局单例ThreadLocal 单例
实例数量整个 JVM 一个每个线程一个
线程安全实例本身需考虑并发天然线程隔离
典型场景配置、连接池SimpleDateFormat、用户上下文
withInitial(SixGenerate::new)

线程第一次调用 get() 时,若当前线程没有绑定值,会执行 SixGenerate::new 创建实例并缓存到当前线程的 ThreadLocalMap 中。

为什么需要 remove()

线程池中的工作线程会被复用。若不在任务结束后 remove()ThreadLocal 中的对象无法被 GC,造成内存泄漏(线程不死,Map 条目一直在)。

try {
    SixGenerate ctx = SixGenerate.getInstance();
    // 使用 ctx ...
} finally {
    SixGenerate.remove(); // 线程池场景务必清理
}
优缺点
优点缺点
无锁,线程内高性能不是真正的全局单例
避免共享状态竞争线程池下需手动 remove
适合「每线程一份」的资源实例总数 = 线程数,占内存

7. 容器式单例

特点:用一个注册表按 key 管理多组单例,适合「多种类型、各保唯一」的场景。

完整代码

package com.zx.generater;

import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Supplier;

/**
 * 容器式单例 - 按 key 管理多个唯一实例
 */
public final class SingletonManager {

    private static final Map<String, Object> INSTANCES = new ConcurrentHashMap<>();

    private SingletonManager() {
    }

    @SuppressWarnings("unchecked")
    public static <T> T getInstance(String key, Supplier<T> creator) {
        return (T) INSTANCES.computeIfAbsent(key, k -> creator.get());
    }
}

使用示例

// 同一 key 始终返回同一实例
ConnectionPool pool = SingletonManager.getInstance("db-pool", ConnectionPool::new);

CacheManager cache = SingletonManager.getInstance("cache", () -> new CacheManager(1024));

// 不同 key 各自唯一
ConnectionPool pool2 = SingletonManager.getInstance("redis-pool", ConnectionPool::new);

关键细节

computeIfAbsent 的原子性

ConcurrentHashMap.computeIfAbsent 对同一 key 的 compute 是原子的:多个线程同时为同一 key 调用时,只有一个 creator.get() 会执行,其余线程拿到已放入 Map 的实例。

Supplier<T> creator 的作用
  • 延迟创建:只有第一次请求该 key 时才执行 creator
  • 灵活传参:可用 lambda 捕获构造参数。
与 Spring 容器的关系

思想类似「IoC 容器按 Bean 名称管理单例」,但这里是轻量级手写版,不依赖框架。

优缺点
优点缺点
一个管理器管多种单例需约定 key 命名,避免冲突
懒加载 + 线程安全类型安全依赖泛型强转
扩展方便生命周期管理需自行处理(无销毁钩子)

七种方式对比

实现懒加载线程安全防反射防序列化复杂度推荐度
DCL 懒汉式⭐⭐⭐
饿汉式⭐⭐⭐
静态内部类⭐⭐⭐⭐⭐
枚举单例极低⭐⭐⭐⭐⭐
防反射/序列化✅*⭐⭐⭐
ThreadLocal按线程场景限定
容器式多实例管理

* 防反射在常规攻击路径下有效,极端顺序下仍有漏洞。


选型建议

需要最简单、最安全的单例?
  └─> 枚举单例(FourGenerate)

需要懒加载 + 普通类写法?
  └─> 静态内部类(ThreeGenerate)

实例必须在类加载时就绪?
  └─> 饿汉式(TwoGenerate)

必须懒加载且熟悉 DCL?
  └─> 双重检查锁定(OneGenerator)

需要抵御反射和序列化?
  └─> 优先枚举;否则 FiveGenerate + 文档说明边界

每个线程独立一份状态?
  └─> ThreadLocal(SixGenerate),记得 remove()

多种单例统一注册管理?
  └─> SingletonManager 容器式

现代 Java 项目的务实建议

  1. 默认首选枚举单例——代码少、JDK 背书、无额外坑。
  2. 需要继承或更多控制时用静态内部类——Holder 模式是业界标准写法。
  3. 避免手写 DCL——除非有明确理由;容易写错 volatile
  4. ThreadLocal 只用于「线程隔离」——不要误当全局单例。
  5. 单例是否仍必要?——在 Spring 等框架中,@Component + 默认单例作用域往往已足够;手写单例更适合工具库、非 Spring 环境或明确要脱离容器控制的场景。

附录:反射破坏单例演示

public class SingletonBreakDemo {

    public static void main(String[] args) throws Exception {
        OneGenerator a = OneGenerator.getInstance();

        Constructor<OneGenerator> ctor = OneGenerator.class.getDeclaredConstructor();
        ctor.setAccessible(true);
        OneGenerator b = ctor.newInstance();

        System.out.println(a == b); // false — 单例被破坏
    }
}

枚举单例在同样操作下会直接抛异常,这也是推荐枚举的原因之一。


更多推荐