Skip to content

ConcurrentHashMap 为什么能实现线程安全?

ConcurrentHashMap 是 Java 中线程安全的哈希表实现,通过以下机制保证线程安全:

  1. 分段锁(Java 1.7)
  • 将数据分成多个段(Segment),每个段独立加锁,不同段之间可以并发操作。
  • 每个段类似于一个小的 Hashtable,锁的粒度更细,减少竞争。
  1. CAS + synchronized(Java 1.8)
  • 放弃分段锁,改用更细粒度的锁(直接锁单个桶的头节点)。
  • 使用 CAS(Compare-And-Swap) 无锁操作初始化数组或插入头节点。
  • 对链表或红黑树的修改使用 synchronized 锁住头节点,保证单线程操作桶内数据。
  1. 原子操作与内存可见性
  • 通过 volatile 关键字保证数组元素的可见性。
  • 使用原子操作(如 putValreplace)确保复合操作的原子性。

Java 1.7 和 1.8 的 ConcurrentHashMap 核心区别

1. 底层数据结构

版本数据结构
Java 1.7分段锁(Segment 数组) + HashEntry 数组 + 链表。
每个 Segment 独立管理一个哈希表。
Java 1.8数组 + 链表 + 红黑树(类似 HashMap 1.8)
锁粒度细化到单个桶(头节点)。

2. 线程安全机制

版本锁机制
Java 1.7分段锁(ReentrantLock:每个 Segment 独立加锁,不同段可并发操作。
并发度由 Segment 数量决定(默认 16)。
Java 1.8CAS + synchronized
- 初始化数组或插入头节点时使用 CAS 无锁操作。
- 修改链表或红黑树时,锁住头节点(synchronized)。
- 锁粒度更细,并发度更高。

3. 哈希冲突处理

版本冲突处理
Java 1.7仅支持链表,冲突时新节点插入链表头部(头插法)。
Java 1.8链表长度 ≥ 8 且数组长度 ≥ 64 时,链表转为红黑树;红黑树节点 ≤ 6 时退化为链表。

4. 扩容机制

版本扩容方式
Java 1.7每个 Segment 独立扩容,扩容时锁住整个段。
Java 1.8支持多线程协同扩容:
- 线程插入数据时发现正在扩容,会帮助迁移数据。
- 使用 ForwardingNode 标记迁移状态。

5. 性能优化

版本优化点
Java 1.7分段锁减少竞争,但并发度受限于段数量(默认 16)。
Java 1.8- 锁粒度更细(单节点锁),并发度更高。
- 红黑树优化查询效率。
- CAS 减少锁竞争。

为什么 Java 1.8 改用 synchronized 而不是 ReentrantLock

  1. 锁粒度细化synchronized 在 Java 1.6 后进行了大量优化(如锁升级、偏向锁等),性能接近 ReentrantLock
  2. 代码简化:减少内存开销(ReentrantLock 需要维护 AQS 队列)。
  3. CAS 无锁化:通过 CAS 实现无锁初始化或头节点插入,减少锁的使用场景。

关键代码示例(Java 1.8)

java
java

复制代码final V putVal(K key, V value, boolean onlyIfAbsent) {
    // 使用 CAS 插入头节点
    if (casTabAt(tab, i, null, new Node<K,V>(hash, key, value)))
        break;
    // 锁住头节点修改链表或红黑树
    synchronized (f) {
        if (tabAt(tab, i) == f) {
            // 插入或更新节点
        }
    }
}

总结对比表

特性Java 1.7Java 1.8
数据结构分段锁(Segment + HashEntry + 链表)数组 + 链表 + 红黑树
锁机制分段锁(ReentrantLock)CAS + synchronized(锁单个桶)
并发度受限于 Segment 数量(默认 16)更高(锁粒度细化到桶)
哈希冲突处理链表链表 + 红黑树
扩容机制段内独立扩容多线程协同扩容
内存开销较高(每个 Segment 维护独立哈希表)较低

为什么 ConcurrentHashMap 线程安全但不需要全局锁?

  1. 分段锁或细粒度锁:仅锁住需要修改的局部数据(如单个桶),其他线程仍可访问其他部分。
  2. CAS 无锁操作:在初始化或头节点插入时避免锁竞争。
  3. 原子性与可见性:通过 volatile 和原子操作保证多线程下的数据一致性。

适用场景

  • 高并发读写场景(如缓存、计数器)。
  • 替代 HashtableCollections.synchronizedMap,性能更高。
  • 需要保证线程安全且对性能有要求的场景。

注意事项

  • 非原子性复合操作:如 putIfAbsent 是原子的,但 getput 的组合操作仍需外部同步。
  • 弱一致性迭代器:迭代器反映的是某一时刻的快照,不保证实时一致性。

通过以上机制,ConcurrentHashMap 在保证线程安全的同时,显著提升了并发性能。

文章来源于自己总结和网络转载,内容如有任何问题,请大佬斧正!联系我