ConcurrentHashMap 是 Java 并发包中提供的线程安全且高效的哈希表实现,适用于多线程环境。其设计结合了分段锁、CAS(Compare-And-Swap)操作和细粒度同步策略,以实现高并发性能。本文将从数据结构、并发机制、关键操作等方面展开详解。
一、数据结构
-
基础结构
与 HashMap 类似,底层采用 Node 数组 + 链表/红黑树:
- Node 数组(table):存储键值对,每个数组元素称为一个"桶"。
- 链表:哈希冲突时,同一桶内的元素形成链表。
- 红黑树:当链表长度 ≥ 8 且数组长度 ≥ 64 时,链表转为红黑树,优化查询效率(退化为链表的阈值为 6)。
-
节点类型
- 普通节点(Node) :存储键值对,
hash
值为正数。 - 转发节点(ForwardingNode) :扩容时标记已迁移的桶,
hash
值为MOVED
(-1)。 - 树节点(TreeBin) :红黑树的根节点,
hash
值为TREEBIN
(-2)。
- 普通节点(Node) :存储键值对,
二、并发控制机制
-
CAS 无锁化操作
- 初始化数组:通过 CAS 确保只有一个线程初始化数组。
- 插入头节点:若桶为空,使用 CAS 插入新节点,避免锁竞争。
-
细粒度锁(synchronized)
- 锁定桶的头节点 :当发生哈希冲突(链表或树已存在),对头节点加
synchronized
锁,保证同一桶的操作线程安全。 - 锁粒度小:不同桶的操作可并发执行。
- 锁定桶的头节点 :当发生哈希冲突(链表或树已存在),对头节点加
-
多线程协作扩容
-
触发条件 :元素数量超过
容量 × 负载因子
(默认 0.75)。 -
扩容过程:
- 创建新数组
nextTable
(容量翻倍)。 - 分配迁移任务:线程通过
transferIndex
领取迁移区间(一个或多个桶)。 - 迁移数据:将旧数组中的元素复制到新数组,遇到转发节点则跳过。
- 完成迁移:所有线程完成后,替换旧数组为
nextTable
。
- 创建新数组
-
三、关键操作流程
-
put 操作
-
步骤:
-
计算键的哈希值(
spread
方法处理哈希码)。 -
若数组未初始化,则初始化(CAS 控制)。
-
定位到桶,若桶为空则 CAS 插入新节点。
-
若桶非空,锁住头节点:
- 链表:遍历链表,更新或插入节点。
- 红黑树:通过
TreeBin
插入节点。
-
检查是否需要扩容。
-
-
并发安全 :CAS 插入头节点 +
synchronized
锁。
-
-
get 操作
- 无锁读取 :直接访问数组元素,依赖
volatile
修饰的next
指针保证可见性。 - 处理转发节点 :若遇到
ForwardingNode
,则到新数组中查找。
- 无锁读取 :直接访问数组元素,依赖
-
size 操作
- 统计方式 :基于
baseCount
和CounterCell[]
(类似 LongAdder 的分段计数)。 - 最终值 :
size = baseCount + ∑CounterCell[i].value
。
- 统计方式 :基于
四、特性与优化
-
线程安全
- 通过 CAS 和
synchronized
实现无锁化和细粒度锁,避免全局锁竞争。
- 通过 CAS 和
-
高效扩容
- 多线程协作:多个线程可同时参与数据迁移,加快扩容速度。
- 迁移标记 :转发节点(
ForwardingNode
)指示其他线程跳过已处理的桶。
-
迭代器弱一致性
- 不抛出异常:迭代过程中允许其他线程修改 Map。
- 数据可能过期:迭代器创建时捕获当前数组状态,不反映后续修改。
-
哈希算法优化
-
spread
方法:将哈希码高 16 位与低 16 位异或,减少冲突概率。arduinojava static final int spread(int h) { return (h ^ (h >>> 16)) & HASH_BITS; }
-
五、与 Hashtable 的对比
特性 | ConcurrentHashMap | Hashtable |
---|---|---|
锁机制 | 分段锁(桶级别) + CAS | 全表锁(方法级 synchronized) |
并发性能 | 高(多线程可操作不同桶) | 低(全局锁导致串行化) |
Null 支持 | 不允许 null 键或值 | 不允许 null 键或值 |
迭代器 | 弱一致性(不抛异常) | 强一致性(可能抛异常) |
扩容策略 | 多线程协作扩容 | 单线程扩容 |
六、使用场景
- 高并发读写:如缓存系统、实时数据处理。
- 替代 Hashtable:需更高并发性能的场景。
- 线程安全 Map:多线程环境下需保证数据一致性。
七、注意事项
- 哈希冲突设计 :确保键的
hashCode()
和equals()
正确实现。 - 容量规划:合理设置初始容量和负载因子,减少扩容次数。
- 避免死锁:尽管锁粒度小,仍需避免跨桶的复合操作导致死锁。
通过上述机制,ConcurrentHashMap 在多线程环境中实现了高性能的线程安全操作,是 Java 并发编程中不可或缺的核心类之一。