Java单例模式与多例模式应用与实现

推荐用双重检查锁(DCL)+ volatile 实现线程安全单例:外层判空减少锁竞争,内层加锁保证唯一性,volatile 防止指令重排序;枚举单例可防反射和序列化攻击但不支持延迟加载与继承。

单例模式怎么写才真正线程安全

Java里最常写的 Singleton,往往在高并发下出问题。不是加了 synchronized 就万事大吉,也不是靠 static 字段就能一劳永逸。

推荐用双重检查锁(DCL)+ volatile,这是 JDK 6+ 下兼顾性能与安全的

写法:

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 防止指令重排序,避免其他线程拿到未初始化完成的对象
  • 两次 if (instance == null) 判断:外层减少锁竞争,内层保证唯一性
  • 禁止反射攻击?可在构造函数里加 if (instance != null) throw new RuntimeException("Singleton already initialized")
  • 序列化破坏单例?需实现 readResolve() 方法返回 instance

Spring 默认是单例,但为什么有时候像“多例”

Spring 的 @Scope("singleton") 是容器级单例——整个 ApplicationContext 中只存在一个 bean 实例,但这个“单例”不等于线程安全,也不等于全局唯一对象引用。

  • 如果 bean 有可变状态(比如含 private List cache),多个线程同时操作它,照样出错
  • Web 环境中,requestsession 作用域的 bean 看起来“每次请求都新建”,其实是 Spring 在每次请求开始时从代理获取新实例,底层仍是工厂管理
  • 想让某个 bean 每次 getBean() 都返回新实例?显式声明 @Scope("prototype"),但注意:prototype bean 的依赖注入仍由容器完成,只是它本身不被缓存

什么时候必须用多例(prototype),而不是硬写 new

手动 new XxxService() 看似简单,但会绕过 Spring 容器的生命周期管理、AOP 代理、依赖注入和事务控制——这在真实项目里几乎不可接受。

  • 需要带不同初始化参数的多个实例?用 ObjectFactoryProvider 延迟获取,Spring 4.3+ 支持自动注入
  • 高频创建/销毁的轻量对象(如 DTO、Builder)?直接 new 更合理,不属于 Spring 管理范畴
  • 测试中模拟不同行为?用 @MockBean@SpyBean 替换 prototype bean,比改代码更可控
  • 注意:prototype bean 内部若持有 singleton bean 引用,那个 singleton 仍是共享的——多例 ≠ 全隔离

枚举实现单例真的万无一失?

Joshua Bloch 在《Effective Java》里力推枚举单例,确实能天然防止反射和序列化攻击,写法极简:

public enum Singleton {
    INSTANCE;
public void doSomething() { /* ... */ }

}

但它也有现实约束:

  • 无法继承(枚举类隐式 final),不能实现需要扩展的抽象基类
  • 不能延迟加载:JVM 加载类时就初始化所有枚举实例,哪怕你永远没调用 INSTANCE
  • 某些框架(如部分 RPC 序列化工具)对枚举支持不一致,可能报 IllegalArgumentException: No enum constant
  • 单元测试中难以 mock 枚举方法,得靠 PowerMock 或改用接口+委托

真要选枚举单例,确保它只做纯粹的状态无关工具类,不涉及外部资源或复杂初始化逻辑。