Java中的并发容器:ConcurrentHashMap详解
字数 1623 2025-11-12 09:36:00

Java中的并发容器:ConcurrentHashMap详解

1. 背景与需求

在多线程环境下,HashMap是非线程安全的,而HashtableCollections.synchronizedMap虽能保证线程安全,但通过全局锁(锁住整个容器)导致性能瓶颈。ConcurrentHashMap(后称CHM)应运而生,旨在实现高并发下的高效读写。


2. 核心设计思想:分段锁与CAS

CHM在不同JDK版本中实现方式不同,但其核心目标一致:减少锁竞争

JDK 7的分段锁(Segment)

  • 结构:CHM内部由多个Segment(默认16个)组成,每个Segment相当于一个独立的HashTable
  • 锁粒度:写操作仅需锁住对应的Segment,其他Segment仍可被并发访问。
  • 读操作:无需加锁(通过volatile保证可见性)。

JDK 8的优化:CAS + synchronized

  • 废弃分段锁:改为直接使用Node数组(类似HashMap的桶数组)。
  • 锁粒度细化:仅对冲突的桶(链表头节点/红黑树根节点)加锁(使用synchronized)。
  • CAS无锁化:在插入、扩容等操作中,大量采用CAS(Compare-And-Swap)实现无锁并发。

3. JDK 8中的关键实现细节

3.1 基本结构

transient volatile Node<K,V>[] table; // 哈希桶数组  
static class Node<K,V> implements Map.Entry<K,V> {  
    final int hash;  
    final K key;  
    volatile V val;  
    volatile Node<K,V> next;  
}  
  • 桶数组table通过volatile保证可见性。
  • 节点Node中的valnext均为volatile,确保读操作总能获取最新值。

3.2 put操作流程

  1. 计算哈希:通过(h ^ (h >>> 16)) & HASH_BITS计算哈希值,避免哈希冲突。
  2. 桶为空时:直接使用CAS尝试插入新节点(无锁化)。
  3. 桶非空时
    • 若桶为链表,使用synchronized锁住头节点,遍历链表:
      • 若key存在则更新值;
      • 否则插入链表尾部。
    • 若桶为红黑树(哈希冲突严重时转换),锁住根节点后插入。
  4. 判断扩容:插入后若链表长度超过8,且桶数组长度≥64,则转换为红黑树;否则触发扩容。

3.3 扩容机制(重点)

  • 多线程协同扩容
    • 当线程A发现需要扩容时,会创建新数组(大小为原2倍),并标记转移状态。
    • 其他线程在执行put/remove操作时,若检测到扩容状态,会协助迁移数据(分摊开销)。
  • 迁移策略:将原桶拆分为高低位链表,直接分配到新数组的对应位置(避免重新哈希)。

3.4 get操作的无锁设计

  • 直接访问volatile字段,无需加锁。
  • 若正在扩容,会优先从新数组(nextTable)中查询数据。

4. 并发安全与性能平衡

  • 弱一致性迭代器:迭代器遍历时可能反映其他线程的更新,但不保证强一致性(避免ConcurrentModificationException)。
  • size()的统计方法:通过分段计算各桶的节点数,可能存在误差(优先保证性能)。

5. 对比其他线程安全容器

容器 锁粒度 适用场景
Hashtable 全局锁 低并发,简单场景
Collections.synchronizedMap 全局锁 兼容旧代码
ConcurrentHashMap 桶级别锁 高并发读写

6. 实战注意事项

  1. 非原子性复合操作:例如map.putIfAbsent()是原子的,但map.computeIfAbsent()可能被阻塞。
  2. 不要用null值:CHM禁止null作为key或value(避免歧义)。
  3. 合理设置初始容量和并发级别(JDK 8中并发级别参数仅用于兼容性)。

通过上述设计,CHM在保证线程安全的同时,显著提升了并发性能,成为Java并发编程中最常用的容器之一。

Java中的并发容器:ConcurrentHashMap详解 1. 背景与需求 在多线程环境下, HashMap 是非线程安全的,而 Hashtable 或 Collections.synchronizedMap 虽能保证线程安全,但通过全局锁(锁住整个容器)导致性能瓶颈。 ConcurrentHashMap (后称CHM)应运而生,旨在实现高并发下的高效读写。 2. 核心设计思想:分段锁与CAS CHM在不同JDK版本中实现方式不同,但其核心目标一致: 减少锁竞争 。 JDK 7的分段锁(Segment) 结构 :CHM内部由多个 Segment (默认16个)组成,每个 Segment 相当于一个独立的 HashTable 。 锁粒度 :写操作仅需锁住对应的 Segment ,其他 Segment 仍可被并发访问。 读操作 :无需加锁(通过 volatile 保证可见性)。 JDK 8的优化:CAS + synchronized 废弃分段锁 :改为直接使用 Node数组 (类似HashMap的桶数组)。 锁粒度细化 :仅对冲突的桶(链表头节点/红黑树根节点)加锁(使用 synchronized )。 CAS无锁化 :在插入、扩容等操作中,大量采用CAS(Compare-And-Swap)实现无锁并发。 3. JDK 8中的关键实现细节 3.1 基本结构 桶数组 table 通过 volatile 保证可见性。 节点 Node 中的 val 和 next 均为 volatile ,确保读操作总能获取最新值。 3.2 put操作流程 计算哈希 :通过 (h ^ (h >>> 16)) & HASH_BITS 计算哈希值,避免哈希冲突。 桶为空时 :直接使用CAS尝试插入新节点(无锁化)。 桶非空时 : 若桶为链表,使用 synchronized 锁住头节点,遍历链表: 若key存在则更新值; 否则插入链表尾部。 若桶为红黑树(哈希冲突严重时转换),锁住根节点后插入。 判断扩容 :插入后若链表长度超过8,且桶数组长度≥64,则转换为红黑树;否则触发扩容。 3.3 扩容机制(重点) 多线程协同扩容 : 当线程A发现需要扩容时,会创建新数组(大小为原2倍),并标记转移状态。 其他线程在执行put/remove操作时,若检测到扩容状态,会协助迁移数据(分摊开销)。 迁移策略 :将原桶拆分为高低位链表,直接分配到新数组的对应位置(避免重新哈希)。 3.4 get操作的无锁设计 直接访问 volatile 字段,无需加锁。 若正在扩容,会优先从新数组( nextTable )中查询数据。 4. 并发安全与性能平衡 弱一致性迭代器 :迭代器遍历时可能反映其他线程的更新,但不保证强一致性(避免 ConcurrentModificationException )。 size()的统计方法 :通过分段计算各桶的节点数,可能存在误差(优先保证性能)。 5. 对比其他线程安全容器 | 容器 | 锁粒度 | 适用场景 | |------|--------|----------| | Hashtable | 全局锁 | 低并发,简单场景 | | Collections.synchronizedMap | 全局锁 | 兼容旧代码 | | ConcurrentHashMap | 桶级别锁 | 高并发读写 | 6. 实战注意事项 非原子性复合操作 :例如 map.putIfAbsent() 是原子的,但 map.computeIfAbsent() 可能被阻塞。 不要用null值 :CHM禁止null作为key或value(避免歧义)。 合理设置初始容量和并发级别 (JDK 8中并发级别参数仅用于兼容性)。 通过上述设计,CHM在保证线程安全的同时,显著提升了并发性能,成为Java并发编程中最常用的容器之一。