☰
java-design-patterns 项目中的 Monostate(单态)模式:用类级静态状态实现“概念单例“的完整指南
2026/10/4 9:26:37 网站建设 项目流程
  • 示例工程
  • 教程

【免费下载链接】java-design-patterns

Design patterns implemented in Java

项目地址:https://gitcode.com/GitHub_Trending/ja/java-design-patterns
点击查看免费下载

Monostate(单态,又名 Borg)是一种创建型设计模式,它通过把类中所有数据成员声明为static,使任意数量的实例都共享同一份状态,从而在保留"可自由 new 多个对象"的外观下获得类似 Singleton 的全局一致状态。本文以 java-design-patterns 仓库的 monostate 模块 为骨架,结合 LoadBalancer 源码 与测试用例,完整讲解该模式的定义、典型实现、适用场景、权衡取舍及与 Singleton、Flyweight 的对比,读完后你将能独立实现并正确评估 Monostate 的适用性。

一、模式概述:它是什么,又叫什么

Monostate 模式是面向对象设计中实现"单例行为"的另一种思路:它不限制实例数量,而是强制所有实例共享同一份状态。

  • 别名:Borg(源自《The ThoughtWorks Anthology》中的叫法)。
  • 本质:wiki.c2.com 给出的经典定义是——"monostate 是一个'概念单例',它的所有数据成员都是静态的,因此该类的所有实例都使用相同(静态)数据。使用 monostate 的应用可以创建任意数量的实例,因为每个实例使用的都是同一份数据。"
  • 与 Singleton 的根本差异:Singleton 通过私有构造器把类的实例数量限制为一个;Monostate 则允许创建多个实例,但确保它们状态完全一致。正如 App.java 的注释 所写:"Monostate 模式确保类的所有实例拥有相同的状态,可以直接作为 Singleton 模式的替代品。"

在仓库中,该模式归入Creational(创建型)分类,同时被标注了 Encapsulation、Instantiation、Object composition、Persistence、Polymorphism 等多个标签,这暗示它既能做创建层面的状态共享,也涉及封装与持久化语义。

二、现实世界类比与直观理解

图书馆类比:想象一座图书馆里摆放着多张查询台,读者可以在任意一张台上查阅馆藏目录。虽然每张台看起来彼此独立,但任何台对目录的修改(如新增或下架一本书)都会立刻反映到所有台上。无论读者使用哪张台,看到的都是同一份最新目录——这正对应 Monostate 模式:类的多个实例共享同一状态,确保所有实例看到的数据始终一致。

用一句大白话说:Monostate 让一个类的多个实例共享同一状态,实现方式是把所有状态管理都汇聚到一个公共的共享结构(静态字段)中,从而在维持"独立对象"外观的同时保证状态全局一致。

这种设计带来的体验是:调用方拿到的是普通实例,写代码的方式与使用普通类完全一致,但背后所有读写都落在类级别的共享内存上。

三、模式流程图

下图是仓库中 Monostate 模式原理说明流程图(monostate/etc/monostate-flowchart.png):分别创建实例 A、实例 B,两个实例都访问共享的静态状态;实例 A 更新共享状态后,实例 B 读取到的是更新后的值——两个对象看似独立,行为却完全基于全局共享状态。

四、仓库源码级实现:LoadBalancer 负载均衡器

monostate 模块 用"负载均衡器 + 轮询派发请求"的场景演示 Monostate。核心类如下:

类文件职责
LoadBalancerLoadBalancer.javaMonostate 载体,持有全部静态状态,按 Round Robin 轮询派发请求
ServerServer.java后端服务器,含 host、port、id,serve(Request)处理请求并打印日志
RequestRequest.javaJavarecord,封装请求负载
AppApp.java程序入口,演示两个实例共享状态

4.1 LoadBalancer 核心实现

仓库中的真实实现(与 README 示例略有差异,这里以源码为准)如下:

public class LoadBalancer { private static final List<Server> SERVERS = new ArrayList<>(); private static int lastServedId; static { var id = 0; for (var port : new int[] {8080, 8081, 8082, 8083, 8084}) { SERVERS.add(new Server("localhost", port, ++id)); } } /** Add new server. */ public final void addServer(Server server) { synchronized (SERVERS) { SERVERS.add(server); } } public final int getNoOfServers() { return SERVERS.size(); } public int getLastServedId() { return lastServedId; } /** Handle request. */ public synchronized void serverRequest(Request request) { if (lastServedId >= SERVERS.size()) { lastServedId = 0; } var server = SERVERS.get(lastServedId++); server.serve(request); } }

要点拆解:

  • 静态字段即共享状态:SERVERS(服务器列表)与lastServedId(下一次派发的游标)都是static,它们不属于任何单个实例,而是属于类本身。所有LoadBalancer实例读写的是同一份内存。
  • 静态初始化块:类加载时一次性向SERVERS注册 5 台localhost:8080~8084的服务器,id 依次为 1~5。这段初始化只执行一次,与实例数量无关。
  • 轮询派发逻辑:serverRequest取出lastServedId指向的服务器,派发请求后将游标+1;当游标越界(>= SERVERS.size())时重置为 0,实现循环轮询(Round Robin)。多个实例交替调用时,会轮流命中 Server 1、Server 2、Server 3……
  • 并发安全:addServer对SERVERS加同步锁,serverRequest是synchronized方法,避免多线程下静态游标被并发改写导致错乱。静态共享状态天然是"全局临界区",因此并发控制必不可少。
  • 透明的实例外观:调用方完全不知道状态是静态的,new LoadBalancer()出来的对象用起来与普通对象无异——这正是"Monostate"(单一状态)命名的由来。

4.2 Server 与 Request:被共享状态驱动的配角

public record Request(String value) {}
@Slf4j @Getter public class Server { public final String host; public final int port; public final int id; public Server(String host, int port, int id) { this.host = host; this.port = port; this.id = id; } public void serve(Request request) { LOGGER.info( "Server ID {} associated to host : {} and port {}. Processed request with value {}", id, host, port, request.value()); } }

Request用 Java 16+ 的record简洁封装请求负载;Server用 Lombok 的@Getter/@Slf4j生成访问器与日志器,serve负责打印"哪台服务器处理了哪个请求",便于观察轮询结果。

4.3 程序入口:两个实例如何共享状态

App.java 只做三件事:创建两个LoadBalancer实例,然后依次派发两个请求:

public static void main(String[] args) { var loadBalancer1 = new LoadBalancer(); var loadBalancer2 = new LoadBalancer(); loadBalancer1.serverRequest(new Request("Hello")); loadBalancer2.serverRequest(new Request("Hello World")); }

预期行为(README 原话):通过loadBalancer1发出的请求由Server 1处理;由于lastServedId已被第一个实例更新,通过loadBalancer2发出的请求由Server 2处理;若再创建第三个实例并派发请求,则会轮到Server 3。这就直观证明了"实例不同、状态同一":每个新实例看到的是已被前一个实例推进过的共享游标。

五、测试用例:如何验证"所有实例状态一致"

LoadBalancerTest.java 给出了两个可复用的验证思路:

1. 跨实例状态一致性测试

final var firstBalancer = new LoadBalancer(); final var secondBalancer = new LoadBalancer(); firstBalancer.addServer(new Server("localhost", 8085, 6)); // Both should have the same number of servers. assertEquals(firstBalancer.getNoOfServers(), secondBalancer.getNoOfServers()); // Both Should have the same LastServedId assertEquals(firstBalancer.getLastServedId(), secondBalancer.getLastServedId());

只通过firstBalancer添加了一台服务器,但断言secondBalancer的服务器数量与游标与之相等——因为二者读的是同一份静态数据。

2. 轮询派发行为测试:用 Mockito 构造 mockServer,连续派发getNoOfServers() * 2次请求,验证每台 mock 服务器恰好被调用 2 次(times(2)),证实轮询按周而复始的循环推进。

此外,AppTest.java 以assertDoesNotThrow(() -> App.main(new String[] {}))保证入口可无异常运行。整个模块依赖 slf4j-api、logback-classic 与 JUnit 5、Mockito(见 monostate/pom.xml),并配置了 maven-assembly-plugin,主类为com.iluwatar.monostate.App,可在模块目录下直接打包运行:

mvn -pl monostate clean package java -jar monostate/target/monostate-1.26.0-SNAPSHOT-jar-with-dependencies.jar

运行后日志会依次显示 Server 1、Server 2 分别处理Hello与Hello World两个请求。

六、何时应该使用 Monostate

根据 monostate/README.md,以下场景适合采用 Monostate:

  1. 需要跨实例共享状态:类的所有实例必须共享同一状态,某个实例的修改要立即反映到所有实例上。相比传统 Singleton,Monostate 更灵活——不限制实例数量。
  2. 希望透明使用:Singleton 的"全局唯一实例"在使用上不够透明;Monostate 让客户端像使用普通实例一样操作,无需感知共享状态的存在。
  3. 需要子类扩展灵活性:这是 Monostate 相对 Singleton 的明显优势——Monostate 的子类可以新增或修改行为,同时仍与基类实例共享同一份状态,从而在同一应用的不同部分演化出多样行为而共享同一份数据。
  4. 想避免全局变量但仍需共享状态:全局变量难以控制访问、难以测试;Monostate 把共享状态收进类内,借助方法访问,规避裸全局变量。
  5. 需要融入既有系统:已有系统若习惯以"实例"为单位协作而非单个全局对象,Monostate 更易无缝接入,重构为共享状态时过渡更平滑。
  6. 需要一致的配置或状态管理:当应用各部分要求配置/状态始终同步时,Monostate 可保证全体一致。

典型真实应用场景:① 需要被应用各部分共享访问的配置设置;② 需要跨多个消费者保持状态一致性的资源池(资源连接池、线程池、对象池等,本仓库的 LoadBalancer 服务器池即属此类)。

七、收益与代价

收益(Benefits):

  • 不限制实例创建,就能提供全局共享状态;
  • 保证所有实例间的数据一致性。

代价(Trade-offs):

  • 隐藏依赖:共享状态带来隐式耦合,使系统更难理解和调试——某个实例改状态,其他实例"莫名其妙"受影响;
  • 失去实例独立性:无法再为不同实例维护彼此独立的状态,灵活性下降。

八、与相关模式的对比

  • Singleton 单例模式:两者都保证"单一共享状态"。Singleton 靠限制实例数量达成,Monostate 靠静态成员共享状态、允许多实例。Singleton 的构造器通常私有、继承受限;Monostate 可正常继承扩展,但状态全局唯一这一点二者一致。
  • Flyweight 享元模式:Flyweight 通过共享(部分)内部状态来降低内存占用,与 Monostate 共享状态的思想同源,但 Flyweight 面向"大量细粒度对象的内存优化",Monostate 面向"多实例行为一致性"。

从 UML 类图(monostate/etc/MonoState.ucls、monostate/etc/monostate.urm.puml)看,LoadBalancer的属性被标记为静态(带S),与Server是一对多关联,与Request是依赖关系,与上文源码完全对应。

九、使用建议与注意事项

  1. 优先考虑 Singleton 还是 Monostate?若只需"一个全局实例"且无需继承,Singleton 更直观、显式;若代码库大量以实例方式协作、且需要子类化扩展共享行为,Monostate 更合适。
  2. 务必处理并发:静态状态是进程级共享临界区,写入操作应加锁(仓库中synchronized即示范),否则多线程下轮询游标、服务器列表会被竞争破坏。
  3. 谨慎引入隐藏依赖:Monostate 的共享是隐式的,规模变大后"某个实例改了状态影响全局"的副作用难以追踪;可用显式命名(如LoadBalancer这类角色化类名)降低迷惑性。
  4. 测试策略:参考仓库测试——用"通过实例 A 修改、断言实例 B 观察到一致结果"的用例直接验证共享语义。

Monostate 与 Singleton 殊途同归,都是"类级别唯一状态"的载体;当你需要"多个对象、一份真相"时,它就是那个兼顾透明性与扩展性的选择。

  • 示例工程
  • 教程

【免费下载链接】java-design-patterns

Design patterns implemented in Java

项目地址:https://gitcode.com/GitHub_Trending/ja/java-design-patterns
点击查看免费下载
上一篇:MasaCtrl 项目常见问题解决方案
下一篇:如何用大数据技术讲好数据故事:7个核心工具与实战指南

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询