happens-before规则不指导编写并发集合,而是决定如何正确使用它;它保障特定操作间的可见性链路,而非全局快照,如ConcurrentHashMap中put与size间无内置happens-before,故size可能返回过期值。

Java 中 happens-before 规则不直接指导你“编写”并发集合框架(那是 JDK 开发者的工作),但它决定了你**如何正确使用**并发集合,以及为什么某些看似等价的写法在多线程下行为截然不同。理解它,才能避开“集合没加锁却好像线程安全”的错觉,也能看懂 ConcurrentHashMap、CopyOnWriteArrayList 等类的设计逻辑。
为什么并发集合不能只靠“内部加锁”就万事大吉
很多初学者以为:只要集合内部用了 synchronized 或 ReentrantLock,所有操作就天然满足 happens-before,外部读写就绝对安全。这是误区。
关键在于:happens-before 保障的是**特定操作对之间的可见性链路**,不是整个对象的全局快照。比如:
-
ConcurrentHashMap.put(k, v)写入一个键值对,只保证该次写操作的结果对后续满足 happens-before 条件的读可见; - 但如果你先调用
map.put("a", 1),再调用map.size(),这两个操作之间**没有内置的 happens-before 关系**(除非你显式同步或使用 volatile 语义); - 所以
size()可能返回过期值——这不是 bug,而是 JMM 允许的优化结果。
并发集合中显式依赖的 happens-before 场景
真正起作用的,是并发集合主动利用或适配了 JMM 的几条规则:
Java Maven 项目审查工作流的通用输入处理。当 Java Maven 项目以 ZIP压缩包或 GitLab 仓库 URL 形式提供且需要...时使用。
立即学习“Java免费学习笔记(深入)”;
-
volatile 变量规则:如
ConcurrentHashMap中的baseCount和数组槽位的Node引用,大量使用Unsafe的 volatile 读写(getVolatile/putOrderedObject),确保单个节点更新对其他线程立即可见; -
锁规则:分段锁(旧版)或
synchronized块(新版部分方法)保证解锁前的所有写,对下一个获取同一锁的线程可见; -
程序顺序规则 + 传递性:集合内部方法中多个 volatile 写通过代码顺序+传递性串成链路,例如:
node.next = newNode→head = node,只要两者都是 volatile 写,且中间无重排序干扰,就能形成 A→B→C 的可见性路径。
你在用并发集合时必须自己补上的 happens-before 链
集合本身不替你维护跨操作的语义一致性。典型场景包括:
-
检查后执行(check-then-act):如
if (!map.containsKey(k)) map.put(k, v);—— 这两个操作之间没有 happens-before,存在竞态窗口;必须改用map.putIfAbsent(k, v),其内部用 CAS 或锁将两步合并为一个原子操作,并建立完整 happens-before; -
迭代与修改混合:
CopyOnWriteArrayList.iterator()返回的迭代器基于创建时刻的快照,它和后续的add()之间没有 happens-before;所以迭代器永远看不到新添加元素,这不是缺陷,而是设计选择——它把可见性边界明确划在了迭代器创建那一刻; -
复合状态判断:想确认 “map 不为空且 size > 5”,不能分开调用
!map.isEmpty()和map.size(),应改用一次遍历或封装为同步块,否则两次调用间可能被其他线程修改。
写自定义并发容器时的关键落地点
如果你真要实现一个线程安全的集合类,happens-before 是接口契约的一部分:
- 每个 public 方法的入口和出口,要明确标注哪些写操作对哪些读操作构成 happens-before;
- 避免仅靠“内部 synchronized”就认为所有字段都自动可见——非 volatile 字段仍可能被重排序或缓存;
- 优先使用
VarHandle或Atomic*类做状态更新,它们天然提供 volatile 语义和 happens-before 保证; - 文档必须写清:哪些操作组合有 happens-before(如
offer()与poll()在队列非空时),哪些没有(如size()与任意修改)。

















