不可变对象能解决多线程并发问题,因其状态构造后不可变,final字段由jmm保证初始化安全,消除竞态条件与同步开销;需确保字段final、类型不可变、类final、无this逸出、不暴露可变内部引用。

不可变对象为什么能解决多线程并发问题
因为它的状态从构造完成那一刻起就锁死了——没有字段能被修改,也就没有“一个线程刚读到一半、另一个线程顺手改掉”的可能。Java 内存模型对 final 字段有特殊保障:构造方法一结束,所有 final 字段的值对其他线程立即可见,无需 synchronized 或 volatile。
- 竞态条件(race condition)直接消失:没有写操作,就没有竞争
- 不需要加锁:省去上下文切换、死锁风险和同步块的维护成本
- CPU 缓存自动有效:其他线程读取时不会拿到过期副本,JMM 保证了 final 字段的初始化安全
怎么写出真正安全的不可变类(不是看起来不可变)
光把字段标成 final 不够,常见翻车点是“内部可变对象逃逸”。比如字段是 final List<string></string>,但外部拿到这个 list 后调用 add(),原始对象状态就变了。
- 所有字段必须是
final,且类型本身也要不可变(优先用String、Integer;若用集合,用ImmutableList.of()或Collections.unmodifiableList()封装) - 类声明为
final,防止子类通过重写方法破坏不可变性 - 构造过程中禁止
this引用逸出(比如在构造器里开新线程、注册监听器、传给静态容器) - 不提供任何 setter、不返回可变内部对象的原始引用(例如不要直接 return
new ArrayList(this.items),而要 returnnew ArrayList(this.items)的防御性拷贝或不可变视图)
反例:SimpleDateFormat 是可变的,哪怕你把它塞进 final 字段,多个线程共用同一个实例仍会出错。
什么时候该用不可变对象,而不是加锁或原子类
适合那些“读远多于写”、且状态变更本质是“全量替换”的场景。比如配置快照、DTO 传输、时间区间、用户权限上下文等。
- 高频共享只读数据:如
Config实例被上百个请求线程同时读取 → 直接用不可变对象,零同步开销 - 需要缓存键(cache key):
hashCode()和equals()稳定,不怕后续被改导致 map 查不到 - 函数式链式调用:像
builder.withTimeout(5000).withRetries(3).build(),每步都返回新对象,天然无副作用 - 不适用场景:高频更新单个字段(如计数器)、需低延迟写入(每次改都要 new 对象有 GC 压力)、已有复杂可变状态难以重构
Java 里最容易忽略的三个坑
很多开发者写了“看似不可变”的类,上线后在高并发下才暴露问题。
-
getters返回了内部可变集合的原始引用,外部一改,整个对象状态就破防了 - 用了
final byte[]或final Date,但数组内容和Date本身可变,没做防御性拷贝 - 序列化/反序列化绕过了构造逻辑:比如用
ObjectInputStream反序列化出对象后,final字段其实已被 JVM 特殊写入,若构造逻辑含校验(如 age > 0),反序列化可能造出非法状态
真要安全,建议用 record(Java 14+),它默认满足不可变核心约束,且编译器帮你挡掉大部分陷阱。










