concurrenthashmap线程安全靠桶级synchronized+cas而非整表锁,读操作无锁,node字段volatile保证可见性;java 8移除segment改用细粒度桶锁,size()为估算值,复合操作需自行同步。

ConcurrentHashMap 的线程安全不是靠 synchronized 整表锁
它不拦着多个线程同时读,也不强制所有写操作排队——这是和 Hashtable 最根本的区别。Java 8 起彻底移除了 Segment,改用更轻量的机制:对数组桶(bin)级别的 synchronized + CAS 操作组合控制写入,而 get() 这类读操作全程无锁。
关键在于:Node 的 val 和 next 字段都声明为 volatile,保证其他线程能立刻看到最新值;putVal() 中先用 CAS 尝试更新头节点,失败才升级为加锁(只锁住当前桶),避免全局阻塞。
为什么 Java 8 后没有 Segment 了,但文档还提“分段锁”
这是术语遗留造成的混淆。早期 JDK 7 的 ConcurrentHashMap 真有 Segment[] 数组,每个 Segment 是一个可重入锁,负责一段哈希槽。但 JDK 8 改为直接在 Node[] table 上做细粒度控制,锁的粒度从“一段数组”缩小到“单个桶”,甚至多数情况连桶锁都不用——靠 CAS 就搞定。
- 你查源码找不到
Segment类,它已被删除 -
size()、containsValue()这些需要遍历全表的方法,不再按顺序锁所有段,而是用counterCells分散计数 + 多次尝试读取 + 必要时加锁扫描 - 扩容时也不是整体迁移,而是由多个线程协作搬运不同桶,通过
transferIndex控制分工
get() 为什么不用锁还能保证看到最新值
因为两个底层保障缺一不可:volatile 可见性 + 不可变性设计。每个 Node 创建后,key、hash 固定不变,val 和 next 是 volatile 写入。即使某次 get() 读到的是旧值,下一次读也必然能看到新值——没有中间态丢失。
立即学习“Java免费学习笔记(深入)”;
但要注意:这不等于强一致性。比如线程 A 执行 put("k", "v1") 后,线程 B 立即 get("k"),可能拿到 "v1",也可能拿到 null(如果还没完成插入),也可能拿到旧值(如果刚覆盖一半)。它只保证“不读到撕裂数据”,不承诺“立即可见”。
容易踩的坑:别把 ConcurrentHashMap 当作原子操作容器来用
很多人误以为 putIfAbsent、computeIfAbsent 这些方法是万能原子操作,其实它们内部仍可能触发初始化、扩容、树化等复杂流程,执行时间不可控,且回调函数(如 mappingFunction)是在锁内执行的——若你在里面做 IO 或长耗时计算,会卡住整个桶,拖慢其他线程。
-
computeIfAbsent("k", k -> slowIoCall())→ 危险!应提前计算好值再传入 -
size()返回的是估算值,高并发下可能不准;真要精确计数,得用mappingCount()(返回long,更准但仍是近似) -
clear()不是原子清空,而是逐桶清理,过程中其他线程仍可读写 - 不能存
null键或null值,否则抛NullPointerException,这点比HashMap更严格
最常被忽略的一点:它的“线程安全”只针对自身结构操作,不延伸到业务逻辑。比如“先 get 再 put”的复合动作,必须自己加锁或改用 compute() 系列方法——否则就是经典的竞态条件。










