ConcurrentHashMap:线程安全的HashMap

发布时间:2026/6/27 8:20:04
ConcurrentHashMap:线程安全的HashMap ConcurrentHashMap线程安全的HashMap目录HashMap 是非线程安全的ConcurrentHashMap 是什么Java 7 的实现分段锁Java 8 的实现CAS synchronizedput 流程拆解和 Hashtable、Collections.synchronizedMap 的区别小结HashMap 是非线程安全的在聊 ConcurrentHashMap 之前我们先搞清楚 HashMap 到底哪里不安全。问题一数据丢失。put操作不是原子性的。它先计算 hash、再找到桶位置、判断 key 是否存在然后写入。如果两个线程同时 put 到同一个桶就可能会互相覆盖掉对方的值。问题二死循环Java 7。Java 7 的 HashMap 在扩容时采用头插法。并发扩容时链表的指针可能形成环形链表之后调用get时就会陷入死循环。Java 8 改成了尾插法修复了环形链表的问题但并发写入仍然不安全。问题三size 不准确。size()返回的值可能不是当前实际的元素数量因为并发修改过程中计数可能不一致。这三个问题的根源是同一个HashMap 没有对并发操作做任何保护。要解决这个问题最暴力的方案是给整个 Map 加一把大锁同一时刻只允许一个线程操作。这样确实是线程安全了但完全不能并发处理性能堪比一根烂香蕉。由此我们的主角ConcurrentHashMap闪亮登场。ConcurrentHashMap 是什么ConcurrentHashMap 是 Java 并发包java.util.concurrent中的线程安全 Map 实现。它的核心设计思想是把锁的粒度缩小不锁整个 Map只锁正在操作的那部分数据。ConcurrentHashMap内部把数据分成若干段或桶对不同段的操作互不干扰只有对同一段的操作才需要竞争锁。这样多个线程可以同时写入不同的段并发度取决于分段的数量。Java 7 的实现分段锁Java 7 的 ConcurrentHashMap 采用了Segment 分段锁的设计。它的内部结构是两层哈希第一层把数据分成 16 个 Segment默认值每个 Segment 本质上就是一个小的 HashMap。第二层才是真正的 key-value 存储。写入时先定位到具体的 Segment然后只锁这个 Segment其他 Segment 的操作完全不受影响。理论上最多支持 16 个线程同时写入并发度 Segment 数量。Segment 继承了 ReentrantLock每个 Segment 就是一把独立的可重入锁。put 操作只需要锁定目标 Segment不需要影响其他 Segment。分段锁的方案在大多数场景下工作得很好但它有一个问题Segment 数量在初始化时就确定了不能动态调整。如果某个 Segment 的数据特别多哈希不均匀这个 Segment 就会成为热点其他线程都在等这一把锁其他 Segment 反而闲着。Java 8 的实现CAS synchronizedJava 8 对 ConcurrentHashMap 做了大幅重构废弃了 Segment 分段锁改用CAS synchronized Node 数组的方案。结构变得更简单了直接一个 Node 数组每个 Node 存一个 key-value 对。链表长度超过阈值默认 8时转成红黑树。这和 HashMap 的结构几乎一样区别在于并发控制的方式。为什么要抛弃分段锁因为分段锁的粒度还是太粗。一个 Segment 内部有多个桶锁住一个 Segment 就锁住了它下面所有的桶。Java 8 直接把锁的粒度缩小到单个桶加锁在链表的头节点或红黑树的根节点并发度从 16 提升到了数组长度默认 16可动态扩容。并发控制用了两种机制CASCompare And Swap用于无竞争的情况。线程先尝试用 CAS 直接写入如果成功就结束了不需要加锁。CAS 是 CPU 级别的原子指令没有系统调用的开销比加锁快得多。synchronizedCAS 失败时说明有竞争退化为对桶头节点加 synchronized 锁。synchronized 在 Java 6 之后做了大量优化偏向锁、轻量级锁、自旋锁性能已经非常好了。put 流程拆解Java 8 ConcurrentHashMap 的put操作整个流程可以分成三种情况情况一桶为空。说明没有竞争直接用 CAS 写入新节点。不加锁开销最小。情况二桶正在扩容。当前线程帮助一起扩容加快扩容速度。情况三桶不为空。说明有竞争其他线程已经在这个桶写入了数据退化为对桶头节点加 synchronized 锁然后在锁内做链表插入或红黑树插入。绝大多数情况是情况一或情况二CAS 一把就搞定了根本不需要加锁。只有在真正有竞争时才退化为 synchronized。这就是 Java 8 方案的精妙之处乐观锁优先悲观锁兜底。和 Hashtable、Collections.synchronizedMap 的区别面试常问ConcurrentHashMap 和 Hashtable 有什么区别维度HashtableCollections.synchronizedMapConcurrentHashMap锁的粒度整个表一把锁整个表一把锁桶级别锁Java 8并发度1串行1串行等于桶数量null key/value不允许允许不允许迭代器一致性强一致强一致弱一致性能差差高Hashtable的问题在于锁太粗。它的put、get、size等方法全部加了synchronized同一时刻只有一个线程能操作 Map。线程 A 在 put 的时候线程 B 的 get 也得排队等着。在读多写少的场景下这种全表锁性能太低。Collections.synchronizedMap本质和 Hashtable 一样只是换了一种写法。它用一个装饰器包装了传入的 Map所有方法都加了synchronized锁的是同一个互斥对象。并发度同样为 1。ConcurrentHashMap的优势在于细粒度锁。读操作完全无锁写操作只锁目标桶不同桶的写入互不干扰。在 16 个桶的默认配置下理论上最多支持 16 个线程同时写入。小结ConcurrentHashMap 的核心设计思想是缩小锁的粒度让无竞争的操作不加锁。Java 7 用分段锁把数据分成 16 段并发度从 1 提升到 16。Java 8 更进一步废弃分段锁改用 CAS synchronized 的组合策略无竞争时 CAS 一把搞定有竞争时只锁单个桶并发度提升到桶数组的长度。读操作通过 volatile 保证可见性完全无锁每一个设计决策都在回答同一个问题在保证线程安全的前提下怎么让并发性能尽可能接近无锁。