07|单例:区分唯一实例、安全发布和共享状态
配套代码:GitHub 仓库 · 本篇完整源码 · 行为测试。使用 JDK 25 与 Gradle,包名为
com.hanserwei.patterns.singleton。
系列导航:Java 25 设计模式学习指南
所有文章入口使用相同的标题长度上限。这个规则体积很小,初始化之后不变化,也不需要按请求重新创建。单例可以为这样的对象提供统一实例,但“到处都能拿到它”也会使依赖变得不明显。
本章选择不可变站点策略作为最小案例。先观察唯一性,再理解它能保证到哪里,最后评估是否应该改为构造器注入。
从问题提炼设计意图
单例控制某个类型的实例数量,并提供获取该实例的入口。本例使用单元素枚举,在同一类加载器定义的类型范围内提供一个实例。
对象职责与协作关系
| 示例角色 | 职责 |
|---|---|
SitePolicy |
单元素枚举,封装站点校验行为 |
INSTANCE |
唯一枚举常量 |
maxTitleLength |
不可变规则值 |
Demo |
通过单例入口调用行为 |
classDiagram
Demo ..> SitePolicy : INSTANCE
SitePolicy : INSTANCE
SitePolicy : accepts(title)
代码思路:变化应该落在哪个对象上
INSTANCE 是枚举常量,由 Java 的枚举初始化机制创建和发布。它避免手写双重检查锁定的复杂性,也无需通过公开构造器允许外部 new。这里展示标准类成员与方法,不依赖 JDK 25 的简化 main 写法。
maxTitleLength 是 final,accepts 只读取规则值和输入参数,没有写入共享可变状态。因此本例并发读取简单。标题长度使用 codePointCount,以 Unicode 码点计数,避免直接把 UTF-16 代码单元数量误称为字符数;它仍不等同于用户感知的字素簇数量。
测试让两个虚拟线程任务返回 SitePolicy.INSTANCE,再比较对象身份。它验证本运行环境中的共享行为;安全发布的依据来自语言初始化语义,不是一次并发测试碰巧没有出错。
关键实现与独立运行
配套仓库中的包名是 com.hanserwei.patterns.singleton,源码目录为 src/main/java/com/hanserwei/patterns/singleton/。仓库地址统一见系列导航。以下展示关键文件的完整内容;其余角色和测试在同一仓库中,每个顶级类型各占一个文件。
SitePolicy.java:
package com.hanserwei.patterns.singleton;
/** 同一类加载器内共享的不可变站点策略. */
public enum SitePolicy {
/** 唯一策略实例,由枚举初始化机制安全发布. */
INSTANCE;
/** 所有调用方共享的标题长度上限. */
private final int maxTitleLength = 80;
/** 返回按 Unicode 码点计算的标题长度上限. */
public int maxTitleLength() {
return maxTitleLength;
}
/** 检查标题非空白且不超过长度上限. */
public boolean accepts(String title) {
return title != null
&& !title.isBlank()
&& title.codePointCount(0, title.length()) <= maxTitleLength;
}
}
Demo.java 展示调用方如何装配这些对象:
package com.hanserwei.patterns.singleton;
/** 演示本章对象的装配方式和可观察结果. */
public final class Demo {
/** 禁止实例化演示入口. */
private Demo() {}
/** 运行独立示例;args 为未使用的命令行参数. */
public static void main(String[] args) {
System.out.println(SitePolicy.INSTANCE.accepts("Java 25"));
}
}
在配套代码仓库根目录运行;Windows 使用 gradlew.bat 替换 ./gradlew:
./gradlew runSingleton
./gradlew test --tests 'com.hanserwei.patterns.singleton.PatternTest'
示例的业务输出如下,省略 Gradle 自身的任务提示:
true
用测试确认模式的行为
并发任务得到相同实例;80 个码点的标题被接受,81 个被拒绝,空白失败。测试同时关注身份与策略行为。
对应测试位于 src/test/java/com/hanserwei/patterns/singleton/PatternTest.java。建议先运行现有测试,再改动一个协作环节,观察哪个断言能够发现问题。
常见用法
- 生命周期覆盖整个应用、初始化后不可变的策略或元数据。
- 确实需要统一身份的进程内协调对象,但仍需明确并发机制。
- 遗留系统中必须提供统一访问入口,同时逐步梳理依赖。
适用边界与容易踩的坑
唯一实例不等于方法线程安全。给这个枚举新增普通 HashMap 后,并发写入仍然需要同步或并发容器。也不能用“只有一个实例”推出复合操作具备原子性。
单例不是集群唯一:多个 JVM、多个类加载器都可能拥有各自的实例。分布式任务选主、全局编号和跨进程锁不能由本模式直接解决。
全局入口会隐藏依赖,测试替换规则也更困难。若调用方依赖一个 Policy 接口,再通过构造器注入同一个实例,既可共享对象,又保留可替换性。依赖注入容器中的 singleton 作用域通常表示容器管理的共享生命周期,不等同于类型本身禁止创建第二个实例。
与相近模式比较
单例回答实例数量和访问方式;享元回答哪些细粒度对象的内在状态可以共享。享元工厂通常管理多个共享对象,不能因为“共享”就把它叫单例。
动手练习
抽取 TitlePolicy 接口,让一个服务通过构造器接收策略;保留枚举作为默认实现,再提供测试策略。比较直接访问 INSTANCE 和注入接口时,调用方测试需要知道多少全局配置。
系列导航:Java 25 设计模式学习指南