设计模式 20:唯一实例属于哪个范围
两个不相关的订单测试都要读货币配置。如果把可写的调用次数计数器塞进进程内的“唯一配置”,第二个调用者会读到第一个调用者留下的值;代码里看不见依赖注入,也难以隔离测试。需求究竟是“在一个明确范围内共用只读配置”,还是“一切场景全局共享可写状态”?
用身份断言限定结论
Before.INSTANCE 存可写 calls。原有反例在一个测试方法内观察计数留存;TestStatePollutionTest 进一步用明确的 JUnit 顺序执行两个测试方法:第一个记录基线并递增,第二个断言读到了前一个留下的值。这组有意依赖顺序的测试用于揭示污染,不作为普通业务测试的组织方式。显式传入两份配置值时不存在这条共享身份路径。
After.instance() 改用静态 Holder,实例保存不可变 CNY 字符串。20 次普通任务验证相同引用与币种;冷启动验证另起一个 JVM,避免前面的测试已经初始化实例。八个任务先在 CyclicBarrier 会合,再同时调用 instance(),结果为构造次数从 0 到 1,八个引用一致。没有使用 sleep 猜测线程交错。
1 | |
Alternative 是显式创建的配置值,两次 new Alternative("CNY") 相等却不是同一引用。需要按请求、测试或租户切换币种时,直接传入这样的值更便于划清生命周期。Singleton 适用于严格确定作用域并且确实要求该范围一个实例的场景;若唯一性只是“在当前 DI 容器里每个服务一份”,不要偷换为 JVM、跨类加载器、跨进程甚至全局唯一。缓存也不因有唯一入口而自动获得并发安全。
| 方案 | 身份与状态 | 代价 |
|---|---|---|
Before |
进程内此类加载器下共享可写计数 | 可能污染不相关调用者 |
After |
此类加载器下共享只读配置 | 无法为不同调用者各配一份币种 |
Alternative |
调用者显式传入每份配置 | 需要传递依赖,范围由调用者管理 |
初始化安全与可变状态是两项合同
Holder 首次被主动使用时才执行类初始化。JLS 21 的类初始化过程规定初始化锁及其他线程的等待规则;本例用这项语言机制发布实例,而不是自行编写双重检查锁。构造计数是教学观测点,不应成为生产单例的全局监控接口。
初始化完成只说明调用者得到了已经构造的对象。如果把计数器放回实例,再执行 calls++,对象身份唯一也不会把读、加、写自动变成原子操作。跨测试共享状态和多线程丢失更新是两个问题;本章完成前者与首次初始化实验,后者在 E04 用受控交错验证。
flowchart LR
Threads[8 个冷启动调用方] --> Instance[After.instance]
Instance --> Holder[Holder 类初始化]
Holder --> One[不可变 After 实例]
Tests[有序测试] --> Global[Before 可写全局 calls]
验证与练习
运行 ./mvnw -B -ntp -pl labs/20 -am test 和累计 ./mvnw -B -ntp verify;环境、退出码及原始输出在 examples/design-patterns/evidence/20/RUN.md。没有测跨类加载器复制、跨 JVM 唯一性、容器 scope 或并发计数正确性,这些均 NOT_RUN。
- 新增“两个租户分别为
CNY、USD”的变化;先写隔离断言,核对旧CNY调用仍成立,再选择显式传配置的方案并复跑合同。 - 为可写计数器增加并发递增需求,先编写可失败的数量断言,再说明仅验证实例身份为何不够;不要从一次通过推出线性一致性。
本次补全增加的实验在 examples/design-patterns/evidence/local-completion-20261003/RUN.md 留存本地命令与原始输出;各章原云端日志保留其历史版本边界。
参考资料
- GoF 原书公开图书馆 PDF,3.5 Singleton,目录页标注起始页 144:https://cpcc.chd.gov.in/Content/PDFs/w8UkV3tWNyEtsbUZSWJ7fVhuB9A3tsGYdm4w6VGzg2wUTNFYikqnvvFbbkiW2zmfspPEghd7QTamiMby3lVIBemrhdVWwt6rOQnm.pdf 。
- JLS 21 §12.4.2 类初始化:https://docs.oracle.com/javase/specs/jls/se21/html/jls-12.html#jls-12.4.2 。
- Singleton 模式简定义,InfoWorld/JavaWorld:https://www.infoworld.com/article/2170730/design-patterns-the-big-picture-part-1-design-pattern-history-and-classification.html 。
- 本篇来源边界:
writing-plans/design-patterns/SOURCES.md。 - 实验代码:
examples/design-patterns/labs/20/。
上一节:19 复制订单时共享了什么;下一节:21 接口转换如何保留语义。






