ConcurrentHashMap.clear() 通过volatile/CAS批量清空槽位而非加锁,本质是无锁、弱一致的“换表”操作;它不阻塞并发读写,旧表由GC回收,isEmpty()才是准确判空方式。

ConcurrentHashMap.clear() 并不逐个节点释放锁,也不需要“逐个槽位解锁”。它的实现逻辑与直觉相反:它不依赖遍历加锁再清空,而是通过批量替换整个哈希表引用 + 保证可见性来完成清空,本质是无锁、高效、原子性弱一致的操作。
下面分几个关键点讲清楚:
clear 方法的核心机制是“换表”,不是“删节点”
-
clear()不会遍历每个Node、不调用remove、不逐个synchronized槽位。 - 它直接将内部的
table数组引用置为一个全新的空数组(长度仍为原容量,但所有槽位为null)。 - 原来的旧
table会被 GC 自动回收(前提是无其他强引用)。 - 这个赋值操作本身是volatile 写(JDK 8+ 中
table字段为volatile),确保其他线程能立即看到新表。
public void clear() {
long delta = 0L; // 用于调整 baseCount(计数器)
int i = 0;
Node<K,V>[] tab = table;
if (tab != null && tab.length > 0) {
for (; i < tab.length; ++i) {
setTabAt(tab, i, null); // CAS 设置每个槽位为 null
}
// 后续还会重置计数器等,但核心就是上面这一步
}
}注意:JDK 8 的
clear()实际上不是简单赋新数组,而是对每个槽位调用setTabAt(tab, i, null)—— 即用Unsafe.putObjectVolatile或 CAS 清空每个位置。这是为了避免因扩容未完成导致的旧表残留问题,同时兼容正在并发 transfer 的场景。
为什么不需要“逐个释放锁”?
- ConcurrentHashMap 没有传统意义上的“槽位锁对象”长期持有。
- 写操作(如
put/remove)只在修改某个 bin 时临时获取该 bin 头节点的synchronized锁(JDK 8+),或使用CAS + synchronized组合,锁的作用域极小、瞬时。 -
clear()本身不获取任何锁,它只是把每个槽位设为null。即使某线程正在put到某个 bin,只要clear先完成对该 bin 的setTabAt(null),后续put会发现头节点为null,走初始化路径重新建链 —— 这是设计允许的竞态,不影响线性一致性(最终状态为空)。
槽位清空过程中的并发安全怎么保障?
- 使用
setTabAt(tab, i, null),底层是Unsafe.putObjectVolatile或U.compareAndSetObject,保证写操作对所有线程立即可见。 - 如果某个 bin 正在被 transfer(扩容),其头节点可能是
ForwardingNode;clear依然会把它设为null,这没问题 ——ForwardingNode本身只是中转标记,清掉不影响正确性。 -
clear过程中若有线程正在get,读到null就返回null;若刚清完某槽位,get可能读到旧值(取决于内存模型和执行时序),但这是弱一致性保证,符合ConcurrentHashMap设计目标(不要求实时强一致)。
清空后 size() 为什么可能不准?如何修正?
-
clear()会尝试重置计数器(baseCount和CounterCell[]),但多线程下仍可能存在微小误差。立即学习“Java免费学习笔记(深入)”;
size()是估算值(基于sumCount()),清空后若仍有未合并的CounterCell,size()可能短暂大于 0。如需精确判断是否为空,应使用
isEmpty()—— 它只检查baseCount == 0 && counterCells == null,更轻量且足够可靠。不建议依赖
size() == 0判断清空完成;isEmpty()是唯一语义明确的判空方式。
总结关键事实
-
clear()是无锁批量清空,靠 volatile/CAS 写每个槽位,不是“逐个加锁再删”; - 它不等待任何正在进行的读写操作,也不阻塞并发
put/get; - 清空后旧数据由 GC 回收,无需手动释放锁或资源;
- 行为是弱一致性:
get可能短暂读到旧值,size()可能延迟归零,但isEmpty()立即准确; - 它的设计目标是高吞吐、低延迟、不牺牲并发性,而非强同步语义。
不复杂但容易忽略:clear() 的“干净”来自内存可见性控制,而不是锁管理。


















