实施 Java 单例非常困难

一则或许对你有用的小广告

欢迎加入小哈的星球 ,你将获得:专属的项目实战 / Java 学习路线 / 一对一提问 / 学习打卡/ 赠书活动

目前,正在 星球 内带小伙伴们做第一个项目:全栈前后端分离博客项目,采用技术栈 Spring Boot + Mybatis Plus + Vue 3.x + Vite 4手把手,前端 + 后端全栈开发,从 0 到 1 讲解每个功能点开发步骤,1v1 答疑,陪伴式直到项目上线,目前已更新了 204 小节,累计 32w+ 字,讲解图:1416 张,还在持续爆肝中,后续还会上新更多项目,目标是将 Java 领域典型的项目都整上,如秒杀系统、在线商城、IM 即时通讯、权限管理等等,已有 870+ 小伙伴加入,欢迎点击围观

单例 是一个仅实例化一次的类,可用于表示“全局”或系统范围的组件。单例的常见用法包括记录器、工厂、窗口管理器或平台组件。我的一般建议是尽可能避免使用单例,因为很难突破或覆盖功能,也很难编写测试模拟,而且它们也往往会在所有代码结构中产生糟糕的结果。正如这篇文章所证明的那样,他们也没有安全感。

已经做出了许多努力来设计良好的单例模式,但是有一种非常简单有效的方法可以做到这一点。然而,确实没有什么坚如磐石的方法可以保证 单例的 完整性不被破坏。阅读这篇文章,看看你是否同意。

最终实地计划

此解决方案依赖于将构造函数保持私有并导出公共静态最终成员以提供如下所示的单例访问:


 public class FooSingleton {
public final static FooSingleton INSTANCE = new FooSingleton();

private FooSingleton() { }

public void bar() { }

}

当类第一次被引用时,静态成员被初始化,私有构造函数只被调用一次。可以保证,即使多个线程可能在初始化之前引用该类,JVM 也会确保在线程继续运行之前正确初始化该类。但是,存在这样的风险,即有人可能会使用 setAccessible(true) 方法使用反射创建该类的虚假实例。这是可以做到的:


 public class FooSingleton {
public final static FooSingleton INSTANCE = new FooSingleton();

private FooSingleton() { }

public void bar() { }

}

我们需要修改构造函数,以防止类被多次构造,例如在再次调用时抛出异常。如果我们像这样修改 FooSingleton,我们就可以防止此类攻击:


 public class FooSingleton {
public final static FooSingleton INSTANCE = new FooSingleton();

private FooSingleton() { }

public void bar() { }

}


虽然这看起来更安全,但创建不需要的类实例是否几乎和以前一样容易。我们可以选择 INSTANCE_CREATED 字段并修改它,然后再执行与之前相同的技巧,如下所示:


 public class FooSingleton {
public final static FooSingleton INSTANCE = new FooSingleton();

private FooSingleton() { }

public void bar() { }

}

这不是一个可行的解决方案,因为我们引入的任何保护都可以使用反射来规避。

静态工厂方案

在这种方法中,公共成员是一个静态工厂,如下所示:


 public class FooSingleton {
public final static FooSingleton INSTANCE = new FooSingleton();

private FooSingleton() { }

public void bar() { }

}

调用 getInstance() 时,它总是返回相同的对象引用。尽管这个方案和之前的方案一样存在反射漏洞,但它还是有一些优势的。例如,您可以在不更改实际 API 的情况下更改单例的实施,正如您将在本文后面看到的那样。从历史上看, getInstance() 方法已用于大多数单例实现,并且按照事实上的惯例,它构成了一个明确的标记,表明这确实是一个 单例

按需初始化持有者习语

如果您想尽可能推迟 单例 的创建(即 惰性初始化 ),您可以使用 Initialization-on-demand Holder Idiom, 其中在首次调用 getInstance() 时以线程安全的方式创建单例。这是对之前在首次引用类时创建单例(即 急切初始化 )的方案的改进。它看起来像这样:


 public class FooSingleton {
public final static FooSingleton INSTANCE = new FooSingleton();

private FooSingleton() { }

public void bar() { }

}

留意可序列化

如果单例实现了可序列化,那么它的 单例 属性就会面临另一种威胁。因此,您需要声明所有字段为瞬态(这样它们就不会被序列化)并提供一个自定义的 readResolve() 方法,它只返回 INSTANCE 引用。

枚举成语

该方案仅使用枚举作为单个 INSTANCE 成员的持有者,如下所示:


 public class FooSingleton {
public final static FooSingleton INSTANCE = new FooSingleton();

private FooSingleton() { }

public void bar() { }

}

根据 Java Language Specification §8.9 “Enum 中的最终克隆方法确保永远无法克隆枚举常量,并且序列化机制的特殊处理确保永远不会因反序列化而创建重复实例。枚举类型的反射实例化被禁止。这四件事共同确保枚举类型的实例不存在于枚举常量定义的实例之外。”

因此,据称我们可以免费获得针对序列化、克隆和反射攻击的保护。当我第一次看到这个说法时,我立刻觉得有必要证明它是错误的。正如您在以下代码片段中所见,绕过保护措施相当容易。


 public class FooSingleton {
public final static FooSingleton INSTANCE = new FooSingleton();

private FooSingleton() { }

public void bar() { }

}

当我们运行代码时,我们得到以下输出:


 public class FooSingleton {
public final static FooSingleton INSTANCE = new FooSingleton();

private FooSingleton() { }

public void bar() { }

}

Enum 方案的一个缺点是我们不能从另一个基类继承,因为枚举不能扩展另一个类。原因是它已经固有地扩展了 java.lang.Enum。如果您想模仿继承,您可能需要考虑我之前在 此处 发布的帖子中描述的 接口混合模式

enum 方案的一个优点是,如果您以后决定使用“dualtons”或“tringletons”,只需添加新的枚举就可以很容易地添加它们。例如,如果你有一个单例缓存,你可能稍后决定引入几层缓存。

结论

尽管很难规避此处显示的一些单例属性保护,但并没有真正的子弹教授单例方案。如果你能想到更好的,请随时对这篇文章发表评论!

枚举为单例提供了一种良好而简单的工具。 Initialization-on-demand holder idiom 提供了一个很好的模式,如果你想使用继承和/或想要 延迟初始化

祝你的单身狗好运!