Java中的并发容器:ConcurrentHashMap详解
字数 1623 2025-11-12 09:36:00
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 基本结构
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中的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并发编程中最常用的容器之一。