Builder 对象本身是线程安全的吗?能不能复用?
简化版
Builder 对象通常不是线程安全的,也不建议在多线程之间共享。它是构建阶段的临时可变对象,推荐每次构建新对象时创建新的 Builder;如果要复用模板,应使用复制构建器或 toBuilder() 这类明确语义的方法。
详细版
Builder 内部会保存临时字段:
builder.name("Tom").age(18)
这些字段在调用链中不断变化,所以 Builder 本身一般是可变对象。可变对象如果被多个线程共享,就可能出现竞态条件。
例如:
User.Builder builder = User.builder();
// 线程 A
User a = builder.name("A").role("ADMIN").build();
// 线程 B
User b = builder.name("B").role("USER").build();
两个线程可能互相覆盖字段,最终结果不可预测。
即使在单线程里,也不建议随意复用 Builder:
User.Builder builder = User.builder().role("ADMIN");
User a = builder.name("A").build();
User b = builder.name("B").build();
b 会继承 ADMIN 角色,这可能是预期,也可能是隐藏 bug。
更推荐:
User a = User.builder().name("A").role("ADMIN").build();
User b = User.builder().name("B").role("USER").build();
如果确实要复用一组默认配置,可以提供模板方法:
User.Builder adminUserBuilder() {
return User.builder().role("ADMIN").enabled(true);
}
每次返回新的 Builder,而不是共享同一个 Builder 实例。
完整版教学
一、为什么 Builder 通常不是线程安全的
线程安全问题来自“共享可变状态”。Builder 恰好就是可变状态的容器。
它会保存这些临时字段:
private String name;
private String role;
private boolean enabled;
每次调用链式方法,都会修改 Builder 自身:
public Builder name(String name) {
this.name = name;
return this;
}
如果多个线程同时改同一个 Builder,就会互相覆盖字段。即使单个赋值操作本身是原子的,也无法保证多个字段组合的一致性。
所以 Builder 的默认使用模型是:单线程、局部变量、短生命周期。
二、最终对象线程安全不代表 Builder 线程安全
很多人会混淆这两个概念。
最终对象如果是不可变的,可能是线程安全的:
public class User {
private final String name;
private final String role;
}
但 Builder 是创建这个不可变对象之前的可变工具:
public static class Builder {
private String name;
private String role;
}
Product 稳定,不代表 Builder 稳定。施工现场可以乱,交付成品可以稳,但不能让多个施工队同时抢同一张施工图。
三、复用 Builder 的风险
复用 Builder 最大的问题是字段残留。
例如:
Message.Builder builder = Message.builder()
.source("system")
.priority("HIGH");
Message m1 = builder.to("userA").content("hello").build();
Message m2 = builder.to("userB").content("hi").build();
如果 source 和 priority 是模板字段,这样可能没问题。但如果某一次又设置了 traceId,下一次忘了清理,就可能带出脏数据。
Message m3 = builder.traceId("T-001").to("userC").build();
Message m4 = builder.to("userD").build(); // 可能意外带上 T-001
这种 bug 很隐蔽,因为代码看起来像是在构建新对象,实际上沿用了旧状态。
四、如何安全表达模板复用
如果确实有模板需求,更好的方式是每次返回一个新 Builder:
public static Message.Builder systemHighPriorityMessage() {
return Message.builder()
.source("system")
.priority("HIGH");
}
调用方拿到的是新 Builder:
Message message = systemHighPriorityMessage()
.to("userA")
.content("hello")
.build();
另一个常见方式是 toBuilder():
Message updated = oldMessage.toBuilder()
.content("new content")
.build();
它表达的是“基于已有对象复制一份并修改部分字段”。这个语义比“偷偷复用旧 Builder”清楚得多。
五、要不要把 Builder 做成线程安全
通常没必要。给 Builder 方法加锁会增加复杂度,也违背它短生命周期的设计。
如果一个 Builder 需要跨线程共享,往往说明设计可以调整:
- 每个线程创建自己的 Builder;
- 把公共配置提取成不可变模板对象;
- 使用工厂方法生成新的 Builder;
- 避免共享可变构建状态。
模式设计里一个很实用的判断是:能通过限制使用方式解决的问题,不要优先用复杂同步机制解决。
六、两线程交错展示字段组合如何被撕裂
共享 User.Builder 时,即使每次 String 引用赋值是原子的,name 与 role 两个字段也不是一个原子事务。线程 A 可能写 name=A,线程 B 随即写 name=B 和 role=USER,A 再 build 就得到本不属于它的组合。
共享 Builder 初始 name=null, role=null
T1: name("A")
T2: name("B")
T2: role("USER")
T1: role("ADMIN")
T2: build() 读取 name=B, role=ADMIN(撕裂组合)
T1/T2 结果取决于交错
即使 Product 是 final 字段也无法修复错误快照
每线程新建 Builder 后状态不共享
模板复用改为工厂每次返回新 Builder
这条时间线把“可变构建阶段”和“稳定成品阶段”分开。Builder 可以反复接收参数,但 Product 只有在所有规则通过后才出现;若 Product 在第一步就被 new 出来再逐项修改,就仍然存在半初始化对象泄漏的窗口。
七、构建契约与对象不变量
| 评审维度 | 本题结论 |
|---|---|
| 必填信息 | 每次构建自己的必填参数,不从其他线程或上次构建继承。 |
| 默认值 | 模板默认值通过方法创建新 Builder,而不是共享可变实例。 |
| 跨字段规则 | build 只能校验读到的组合是否合法,不能知道字段属于哪个线程意图。 |
| 可变引用处理 | Product 构造时复制集合,避免 Builder 构建后继续修改影响旧 Product。 |
| Builder 生命周期 | 线程局部、短生命周期、通常单次或明确复制语义。 |
| Product 交付保证 | 不可变 Product 可安全共享,但前提是快照本身构建正确。 |
build() 不是形式上的结束标记,而是对象从“参数集合”变成“合法业务值”的原子边界。单字段输入可以提前拒绝明显错误,跨字段规则必须等信息齐全后判断,Product 私有构造器还应保留必要兜底,避免未来新增创建入口绕过不变量。
八、与构造器、JavaBean 和工厂的选择边界
- 字段少且全部必填时,短构造器或 record 通常比 Builder 更直接。
- 字段多、可选项多、同类型参数易错时,命名步骤能显著提升调用可读性。
- JavaBean setter 适合某些框架绑定,但对象可能在设置完成前就被观察到。
- Builder 关注“同一种复杂对象怎样组装”;工厂模式关注“创建哪一种产品实现”。
- 两者可以组合:工厂选择具体产品族或 Builder,Builder 再完成复杂组装。
- 经典 Director 只有在固定步骤序列需要复用或存在多种表示时才有价值。
- 链式
return this只是语法,校验、默认值、拷贝和收口才是设计语义。 - 若 Builder 比 Product 规则还复杂,应重新拆分对象职责,而不是继续堆方法。
不要优先给 Builder 全面加锁;让每个线程拥有自己的 Builder,公共模板提取为不可变配置或新建函数。
九、交付前的代码与测试检查
- 缺少每个必填字段分别调用
build(),应得到明确且稳定的异常。 - 完全不设置可选字段,核对默认值来自唯一权威位置。
- 传入最小值、最大值和越界值,确认范围判断没有反向或 off-by-one。
- 构造两个互相冲突的字段组合,确认只在信息完整时执行跨字段校验。
- 传入集合或数组后修改原引用,已构建 Product 不应跟着变化。
- 若 getter 返回可变数据,再尝试修改返回值,内部状态仍应保持不变。
- 连续调用同一个 Builder 两次,确认语义是明确允许复制还是文档明确禁止复用。
- 两线程共享一个 Builder 做压力测试应被禁止或证明安全,不能靠偶然结果。
- 若使用 Lombok,检查生成代码、默认值、
@Singular、构造器可见性和框架兼容。 - 若迁移旧 API,比较默认值、异常类型、序列化字段和所有旧调用点行为。
记忆钩子:成品 final 只能冻结结果,不能纠正施工时被其他线程写乱的参数。
十、常见误区与追问
- 误区:Product 不可变所以 Builder 也线程安全。 二者是不同对象,Builder 保存共享可变构建状态。
- 误区:字段赋值原子就不会出现竞态。 多个字段组合和调用序列并不原子。
- 误区:复用 Builder 只会继承明确默认值。 任何上次设置后未重置字段都可能形成脏数据。
- 追问:toBuilder 为什么更清楚? 它显式表达从一个已知 Product 快照复制并修改,而非隐藏残留状态。
- 追问:能把 Builder 做成线程安全的吗? 技术上可加锁或用不可变 Builder,但通常成本高于每线程新建。
- 追问:多次 build 是否允许? 由 API 明确定义;若允许,必须说明后续修改是否影响已建对象并做好拷贝。
十一、加强记忆
Builder 是可变施工图,Product 可以是不可变成品。Builder 默认不线程安全,也不建议复用;要复用默认配置,就每次生成新 Builder,或用 toBuilder() 明确表达复制修改。