饿汉式类加载即初始化、线程安全但可能浪费资源;懒汉式延迟创建需dcl+volatile保障线程安全;静态内部类兼顾延迟与安全;枚举单例防反射反序列化但不支持继承和传参。

饿汉式单例:类加载时就初始化,线程安全但可能浪费资源
饿汉式在 static 字段声明时直接 new 实例,JVM 类加载阶段就完成初始化。它天然线程安全,不用加锁,但缺点也很实在:不管用不用得到,实例都会被创建。
常见错误是误以为“只要加了 private static final 就万无一失”,其实如果构造函数抛异常,会导致类加载失败,后续所有对该类的引用都会触发 NoClassDefFoundError。
适用场景:实例创建开销小、必然会被使用、对启动时间不敏感。
private static final Singleton instance = new Singleton();- 构造函数必须是
private,否则外部可绕过单例控制 - 不要在构造函数里做耗时或可能失败的操作(如读配置、连数据库)
懒汉式单例:第一次调用才创建,需手动处理线程安全
懒汉式把实例创建延迟到 getInstance() 第一次被调用时,节省资源,但默认写法(只判空再 new)在多线程下会生成多个实例——这是最常踩的坑。
立即学习“Java免费学习笔记(深入)”;
典型错误现象:Singleton.getInstance() != Singleton.getInstance() 返回 false,或者日志里看到构造函数被执行了多次。
解决办法不是简单加 synchronized 在方法上(性能差),而是用双重检查锁定(DCL)+ volatile:
-
private static volatile Singleton instance;—— 必须加volatile,否则可能因指令重排序导致其他线程看到未初始化完成的对象 - 外层判空不加锁,内层判空才加锁,避免每次调用都同步
- JDK 1.5+ 才真正支持
volatile的禁止重排序语义,老版本 DCL 无效
为什么推荐静态内部类方式?它比懒汉式更简洁且线程安全
静态内部类利用 JVM 类加载机制实现延迟加载和线程安全,代码少、无同步开销、不依赖 volatile,是实际项目中更推荐的写法。
原理是:外部类加载时,SingletonHolder 并不会被加载;只有调用 getInstance() 时,JVM 才去加载这个内部类,而类初始化过程天然串行、线程安全。
private static class SingletonHolder { static final Singleton INSTANCE = new Singleton(); }- 返回
SingletonHolder.INSTANCE即可,无需任何同步关键字 - 不适用于需要传参初始化的场景(因为类加载无法传参)
- 比枚举单例稍重一点,但比 DCL 更易理解和维护
枚举单例:防反射、防序列化,但灵活性最低
用 enum 实现单例,JVM 保证全局唯一、线程安全、还能天然防止反射攻击和反序列化破坏——这是它不可替代的优势。
但代价明显:不能继承、不能实现接口(除非接口方法全抽象)、不能延迟加载(枚举常量也是类加载时初始化)、构造函数不能抛受检异常。
-
public enum Singleton { INSTANCE; }—— 这就是全部实现 - 如果已有类结构,强行改成枚举会破坏继承体系,慎用
- Android 开发中注意:某些低版本 Dalvik 对枚举有额外内存开销,但 ART 环境已无此问题
真正难的不是写出某种单例形式,而是判断该用哪一种。饿汉式适合工具类,懒汉式 DCL 要小心 volatile 和 JDK 版本,静态内部类平衡了安全与简洁,枚举则胜在防御性。选错的后果往往不是编译不过,而是上线后偶发多实例、内存泄漏或反序列化失败——这些都不会在单元测试里轻易暴露。










