如何正确实现单线程环境下的 Singleton 模式

在单线程环境中,真正的 singleton 必须确保整个 jvm 中仅存在唯一实例,而关键保障在于私有构造函数——它阻止外部通过 `new`

创建新对象,否则即使 `getinstance()` 返回同一引用,也无法避免非法实例的产生。

Singleton 模式的核心契约是:全局唯一实例 + 严格可控的创建入口。仅靠“最终返回同一个对象”并不构成 Singleton;必须从源头杜绝非法实例的诞生。

✅ 正确实现(单线程安全):

public class Singleton {
    private static Singleton instance;

    // 关键:私有构造函数,禁止外部实例化
    private Singleton() {}

    public static Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}

该实现满足 Singleton 三要素:

  • 唯一静态引用 instance
  • 私有构造器封禁 new Singleton() 路径
  • 全局统一获取入口 getInstance()

❌ 错误实现(看似“复用”,实则破坏 Singleton):

public class BrokenSingleton {
    static BrokenSingleton s;

    // ❌ 缺少 private 修饰!编译器自动生成 public 默认构造器
    BrokenSingleton() {} // 实际等价于 public BrokenSingleton()

    BrokenSingleton getInstance() {
        if (s == null) {
            s = new BrokenSingleton(); // 第一次调用才初始化
        }
        return s;
    }

    public static void main(String[] args) {
        BrokenSingleton s1 = new BrokenSingleton(); // ✅ 合法!创建第1个非法实例
        BrokenSingleton s2 = new BrokenSingleton(); // ✅ 合法!创建第2个非法实例
        BrokenSingleton s3 = s1.getInstance();       // 返回共享实例(但 s1、s2 已独立存在)
    }
}

问题本质在于:s1 和 s2 是两个完全独立、互不相关的对象,它们与 s 引用无任何关联。getInstance() 的返回值无法“覆盖”已存在的对象身份——Java 中对象生命周期由引用计数与可达性决定,而非变量赋值。

⚠️ 注意事项:

  • 即使在单线程下,无私有构造器 = 无 Singleton;语言层面无法约束用户不调用 new。
  • getInstance() 方法若为非静态(如示例中),则每个实例都可调用它,进一步削弱控制力——推荐始终使用 static 工厂方法。
  • 此实现不适用于多线程环境(存在竞态条件),如需线程安全,应使用双重检查锁(DCL)、静态内部类或枚举等方式。

总结:Singleton 不是“最后用了同一个对象”,而是“从始至终只允许一个对象被创建”。私有构造函数不是可选项,而是模式成立的必要前提。