1. 简介

本文将快速介绍两种最主流的纯Java单例模式实现方式

2. 基于类的单例模式

最常见的方式是创建一个普通类并确保它满足以下条件:

  • 私有构造函数
  • 静态字段存储唯一实例
  • 静态工厂方法获取实例

我们添加一个info属性用于后续演示。实现如下:

public final class ClassSingleton {

    private static ClassSingleton INSTANCE;
    private String info = "Initial info class";
    
    private ClassSingleton() {        
    }
    
    public static ClassSingleton getInstance() {
        if(INSTANCE == null) {
            INSTANCE = new ClassSingleton();
        }
        
        return INSTANCE;
    }

    // getters and setters
}

⚠️ 虽然这是常见实现,但在多线程场景下存在严重问题——而这正是使用单例模式的核心场景。简单说,它可能创建多个实例,破坏单例原则。虽然可以用锁解决,但下一种实现能从根本上避免这个问题。

3. 枚举单例模式

另一种巧妙的方式是使用枚举实现

public enum EnumSingleton {
    
    INSTANCE("Initial class info"); 
 
    private String info;
 
    private EnumSingleton(String info) {
        this.info = info;
    }
 
    public EnumSingleton getInstance() {
        return INSTANCE;
    }
    
    // getters and setters
}

✅ 这种实现由枚举机制天然保证:

  • 线程安全
  • 序列化安全

完美解决了基于类实现的问题。

4. 使用方式

使用ClassSingleton只需静态获取实例:

ClassSingleton classSingleton1 = ClassSingleton.getInstance();

System.out.println(classSingleton1.getInfo()); //Initial class info

ClassSingleton classSingleton2 = ClassSingleton.getInstance();
classSingleton2.setInfo("New class info");

System.out.println(classSingleton1.getInfo()); //New class info
System.out.println(classSingleton2.getInfo()); //New class info

EnumSingleton的使用方式与普通枚举一致:

EnumSingleton enumSingleton1 = EnumSingleton.INSTANCE.getInstance();

System.out.println(enumSingleton1.getInfo()); //Initial enum info

EnumSingleton enumSingleton2 = EnumSingleton.INSTANCE.getInstance();
enumSingleton2.setInfo("New enum info");

System.out.println(enumSingleton1.getInfo()); // New enum info
System.out.println(enumSingleton2.getInfo()); // New enum info

5. 常见陷阱

单例模式看似简单,实际开发中容易踩坑。问题可分为两类:

  • 设计层面(是否真的需要单例?)
  • 实现层面(是否正确实现了单例?)

5.1. 设计层面问题

本质上单例是一种全局变量。众所周知,全局变量应尽量避免使用,尤其是可变状态的全局变量。

我们不是说要彻底禁用单例,而是提醒:可能有更优雅的代码组织方式。

如果方法依赖单例对象,为什么不直接作为参数传入?这样能显式声明依赖关系,测试时也方便mock这些依赖。

典型踩坑场景:用单例存储应用配置(如数据库连接)。如果作为全局对象使用,测试环境将难以切换配置。最终导致测试数据污染生产数据库——这显然不可接受。

如果必须用单例,考虑将其实例化委托给专门的工厂类,由工厂保证单例的唯一性。

5.2. 实现层面问题

即使看似简单的单例实现,也可能因各种问题导致出现多个实例。

同步问题
前文私有构造函数的实现非线程安全。单线程环境没问题,但多线程下必须同步操作:

public synchronized static ClassSingleton getInstance() {
    if (INSTANCE == null) {
        INSTANCE = new ClassSingleton();
    }
    return INSTANCE;
}

注意synchronized关键字。方法包含多个操作(比较、实例化、返回),若无同步:

  • 线程A和线程B可能同时判断INSTANCE == nulltrue
  • 导致创建两个ClassSingleton实例

❌ 同步会显著影响性能。高频调用场景可考虑:

  • 懒加载
  • 双重检查锁定(⚠️ 注意:因编译器优化可能失效)

多实例问题
JVM层面也可能导致意外多实例:

  1. 单例本应JVM内唯一,但在分布式系统或基于分布式技术的系统中可能失效
  2. 不同类加载器可能加载各自的单例版本
  3. 单例可能被GC回收(无引用时)。虽然不会同时存在多实例,但重建后的实例状态可能与之前不同

6. 总结

本文聚焦纯Java实现单例模式的核心方案,探讨了如何保证一致性及实际应用场景。选择实现方式时需权衡线程安全、序列化需求和性能开销。


原始标题:Singletons in Java | Baeldung