类 | 线程安全 | 并发度 | 备注 |
|---|---|---|---|
HashMap | × | —— | 最快,但多线程下 CPU 100% 死循环曾坑哭无数码农 |
Hashtable | √ | 低 | 全表一把锁,基本等同于串行 |
Collections.synchronizedMap | √ | 低 | 同上,只是包装器 |
ConcurrentHashMap | √ | 极高 | 分段/桶级锁 + CAS,读写可并行,扩容亦并发 |
结论:并发场景下,CHM 是唯一“能扛流量又能打”的哈希表实现。
synchronized 锁单个首节点,冲突时才加锁;扩容时多线程协同迁移,CPU 利用率拉满。// 位标识
staticfinalint MOVED = -1; // ForwardingNode
staticfinalint TREEBIN = -2; // 红黑树根
staticfinalint RESERVED = -3; // computeIfAbsent 占位
transientvolatile Node<K,V>[] table; // 主哈希桶
privatetransientvolatile Node<K,V>[] nextTable; // 扩容目标数组
privatetransientvolatilelong baseCount; // 无冲突时直接累加
privatetransientvolatileint sizeCtl; // 控制标识:负数=正在扩容/-1=初始化/n=扩容阈值
CAS 将 sizeCtl 从 0 改为 -1,单线程建表,其余线程 Thread.yield() 自旋等待。spread(key.hashCode()) 再散列,减少哈希碰撞。(n - 1) & hash,与 HashMap 一致。CAS 插入新节点,失败则自旋。helpTransfer)。
否则 synchronized 锁定首节点,再按链表/红黑树逻辑插入或更新。addCount(1L, binCount),若超过 sizeCtl 则触发 transfer。ForwardingNode 去 nextTable 重新查找。可见性保障:Node.val 用 volatile 修饰;扩容时旧表只读,新表发布前通过 Unsafe 的 volatile 写保证。
transferIndex
从高位往低位分配任务包,CAS 抢占。synchronized,构建新链表/树,插入到新表,旧桶头插 ForwardingNode 占位。MOVED,当前线程领取stride步任务,加快迁移。sumCount() 汇总。方向 | 阈值 | 说明 |
|---|---|---|
链表 → 树 | 8 | 且桶总数 ≥ 64,否则优先扩容 |
树 → 链表 | 6 | 删除节点后检查 |
复合操作非原子
if (!map.containsKey(k)) map.put(k, v); // 仍有竞态
正确姿势:putIfAbsent(k, v) 或 computeIfAbsent。
遍历一致性
Iterator 弱一致性:创建后其他线程增删改不抛 ConcurrentModificationException,但未必可见。
自定义对象作为 key
必须保证不可变且正确实现 hashCode/equals,否则扩容前后定位桶不一致导致“幽灵丢失”。
批量操作
forEach、search、reduce 支持并行化,可指定 parallelismThreshold,在大数据量场景比串行循环快 2~3 倍。
测试场景:32 线程,各执行 1 千万次 put。
实现 | 耗时 (ms) | 吞吐量 (ops/us) |
|---|---|---|
Hashtable | 28 000 | 0.36 |
CHM 1.7 | 4 100 | 2.44 |
CHM 1.8 | 2 300 | 4.35 |
结论:1.8 桶级锁 + 协同扩容,比 1.7 分段锁再提 45%+。
synchronized 做了大量偏向锁/轻量锁优化,锁定时间极短(仅链表/树操作),CAS 失败概率低。ConcurrentHashMap = 哈希表 + CAS 无锁 + 桶级 synchronized + 协同扩容 + 分散计数器, 是 Doug Lea 为 Java 并发世界打造的“性能怪兽”,也是每个 Javaer 必须通关的“面试必考题”。
如果这篇文章帮到了你,记得点个赞并关注。