ConcurrentHashMap详解
ConcurrentHashMap 是 Java 集合框架中的一个线程安全的高性能哈希表实现,广泛用于多线程环境中。它在 JDK 1.5 引入,并在 JDK 1.8 进行了重大优化。以下是对 ConcurrentHashMap 的详细设计分析,涵盖其核心原理、数据结构、并发机制和关键优化点。
1. 核心设计目标
ConcurrentHashMap 的设计目标是在高并发场景下提供高效的线程安全操作,同时尽量减少锁的开销。它与 Hashtable 和 Collections.synchronizedMap 的全锁机制不同,采用 分段锁(JDK 1.5~1.7)或 细粒度锁+无锁机制(JDK 1.8+)来提高并发性能。主要特点包括:
- 高并发性:允许多个线程同时读写,读操作通常无锁,写操作尽量减少锁粒度。
- 线程安全:保证数据一致性,避免并发修改异常。
- 高性能:通过优化锁机制和数据结构,减少阻塞和竞争。
- 无阻塞读:读操作不加锁,依赖内存可见性和 volatile 语义。
- 动态扩展:支持在运行时动态调整容量。
2. 数据结构
ConcurrentHashMap 的底层数据结构在不同 JDK 版本中有显著变化。
JDK 1.5 ~ 1.7:分段锁(Segment Locking)
- 基本结构:ConcurrentHashMap 由多个 Segment 组成,每个 Segment 是一个独立的哈希表,包含一个桶数组(table),每个桶存储键值对(Entry)。
- Segment:Segment 是一个继承自
ReentrantLock的类,每个 Segment 管理一部分桶(table 的一部分)。默认有 16 个 Segment(concurrencyLevel),可以通过构造方法指定。 - 桶(Bucket):每个桶是一个链表,存储键值对,解决哈希冲突采用 拉链法。
- 并发机制:
- 读操作:无锁,依赖 volatile 语义保证内存可见性。
- 写操作:对目标 Segment 加锁,锁粒度是整个 Segment,而不是整个哈希表。
- 扩容:仅对单个 Segment 扩容,不影响其他 Segment。
- 缺点:
- Segment 数量固定(构造时指定),无法动态调整。
- 锁粒度仍然较大(锁定整个 Segment)。
- 迭代器是弱一致性的,可能不反映最新更新。
JDK 1.8+:Node 数组 + 红黑树 + CAS
JDK 1.8 对 ConcurrentHashMap 进行了大幅优化,摒弃了 Segment 机制,采用更细粒度的锁和无锁操作,性能显著提升。
- 基本结构:
- 核心是 Node 数组(table),每个元素是一个桶,可能存储:
- Node:普通链表节点,用于拉链法解决哈希冲突。
- TreeNode:红黑树节点,当链表长度超过阈值(默认 8)时,链表转为红黑树。
- ForwardingNode:扩容时的特殊节点,用于标记桶正在迁移。
- table:存储键值对的数组,初始容量为 16,可动态扩容。
- 核心是 Node 数组(table),每个元素是一个桶,可能存储:
- 哈希冲突处理:
- 链表:当冲突较少时,采用链表存储(Node 节点)。
- 红黑树:当链表长度超过 8 且 table 容量大于等于 64 时,转换为红黑树(TreeNode),提高查询效率(从 O(n) 到 O(log n))。
- 并发控制:
- 读操作:无锁,依赖 volatile 语义(Node 的 val 和 next 字段是 volatile)。
- 写操作:
- 使用 synchronized 锁锁定单个桶(Node 级别),锁粒度比 Segment 更细。
- 使用 CAS(Compare-And-Swap) 机制进行无锁更新,例如初始化 table 或插入节点。
- 扩容:支持并发扩容,多线程协同完成数据迁移。
- 特殊节点:
- ForwardingNode:标记桶已迁移,读线程遇到时会跳转到新 table。
- TreeBin:红黑树的根节点,包含对红黑树的加锁操作。
3. 关键操作原理
以下是 ConcurrentHashMap 的核心操作及其实现细节。
初始化
- table 初始化:
- table 在第一次 put 操作时延迟初始化(lazy initialization)。
- 使用 CAS 操作(sizeCtl 字段)确保只有一个线程初始化 table。
- sizeCtl:控制表初始化和扩容的标志,负数表示正在初始化或扩容。
- 并发级别:JDK 1.8 不再依赖构造方法中的 concurrencyLevel(仅用于向后兼容),而是动态调整锁粒度。
put 操作
- 计算哈希:对 key 的 hashCode 进行扰动(spread 方法),减少哈希冲突。
- 定位桶:通过
(n-1) & hash计算桶索引。 - 插入逻辑:
- 如果桶为空,使用 CAS 插入新节点。
- 如果桶不为空:
- 加锁(synchronized)当前桶的头节点。
- 如果是链表,遍历插入或更新。
- 如果是红黑树,调用红黑树插入逻辑。
- 如果链表长度超过 8,触发树化(treeifyBin)。
- 扩容检查:
- 插入后检查 size 是否超过阈值(loadFactor * capacity)。
- 如果需要扩容,触发 transfer 操作。
get 操作
- 无锁操作,依赖 volatile 语义。
- 过程:
- 计算哈希,定位桶。
- 如果桶头节点匹配,直接返回。
- 如果是链表,遍历查找。
- 如果是红黑树,调用红黑树查找逻辑。
- 如果遇到 ForwardingNode,跳转到新 table 查找。
扩容(resize)
- 触发条件:
- 插入新键值对后,size 超过阈值。
- 链表长度过长(转为红黑树前可能触发扩容)。
- 过程:
- 创建新 table(通常容量翻倍)。
- 多线程协同迁移:
- 每个线程领取一段桶区间(transferIndex 控制)。
- 使用 ForwardingNode 标记已迁移的桶。
- 迁移时对单个桶加锁(synchronized)。
- 迁移完成后,更新 table 引用。
- 优化:
- 多线程并发迁移,分担工作量。
- 读线程在扩容期间仍可访问旧表或新表,保持一致性。
size 操作
- 计数机制:
- 使用 CounterCell 数组(类似 LongAdder)记录增量更新,分散计数热点。
- 最终 size 通过 sumCount 方法汇总。
- 弱一致性:size 不保证实时精确,适合高并发场景。
4. 并发优化
ConcurrentHashMap 的高性能源于以下优化:
- 细粒度锁:JDK 1.8 使用 synchronized 锁定单个桶,取代 Segment 锁。
- CAS 操作:初始化、插入、计数等操作大量使用 CAS,避免锁竞争。
- 无锁读:读操作不加锁,依赖 volatile 保证可见性。
- 红黑树:当哈希冲突严重时,链表转为红黑树,提升查询效率。
- 并发扩容:多线程协同迁移,分担扩容开销。
- CounterCell:分散计数,避免单点竞争。
5. 关键字段和参数
- table:存储键值对的数组,volatile 保证可见性。
- sizeCtl:
- 正数:表示初始化容量或扩容阈值。
- -1:表示正在初始化。
- 负数(-(1 + n)):表示 n 个线程正在扩容。
- loadFactor:默认 0.75,控制扩容时机。
- TREEIFY_THRESHOLD:默认 8,链表转为红黑树的阈值。
- UNTREEIFY_THRESHOLD:默认 6,红黑树退化为链表的阈值。
- MIN_TRANSFER_STRIDE:默认 16,扩容时每个线程处理的最小桶数。
6. 与 HashMap 的区别
- 线程安全:
- HashMap 非线程安全,可能在并发修改时导致死循环或数据丢失。
- ConcurrentHashMap 线程安全,适合多线程环境。
- 锁机制:
- HashMap 无锁。
- ConcurrentHashMap 使用 CAS 和细粒度锁。
- 性能:
- HashMap 单线程性能略高(无同步开销)。
- ConcurrentHashMap 在多线程场景下性能优于 Hashtable 和 synchronizedMap。
- 迭代器:
- HashMap 的迭代器是 fail-fast,并发修改抛出 ConcurrentModificationException。
- ConcurrentHashMap 的迭代器是弱一致性,允许并发修改。
7. 适用场景
- 高并发读写:如缓存、配置管理、计数器等。
- 需要线程安全但不想全锁:比 Hashtable 和 synchronizedMap 更高效。
- 大规模数据:红黑树优化适合大数据量场景。
8. 源码关键点
以下是一些关键方法的伪代码(以 JDK 1.8 为例):
putVal
final V putVal(K key, V value, boolean onlyIfAbsent) {
if (key == null || value == null) throw new NullPointerException();
int hash = spread(key.hashCode());
int binCount = 0;
for (Node<K,V>[] tab = table;;) {
Node<K,V> f; int n, i, fh;
if (tab == null || (n = tab.length) == 0)
tab = initTable();
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
if (casTabAt(tab, i, null, new Node<K,V>(hash, key, value, null)))
break; // 无锁插入
}
else if ((fh = f.hash) == MOVED)
tab = helpTransfer(tab, f); // 协助扩容
else {
V oldVal = null;
synchronized (f) { // 锁定桶头节点
if (tabAt(tab, i) == f) {
if (fh >= 0) { // 链表
binCount = 1;
for (Node<K,V> e = f;; ++binCount) {
K ek;
if (e.hash == hash && ((ek = e.key) == key || key.equals(ek))) {
oldVal = e.val;
if (!onlyIfAbsent)
e.val = value;
break;
}
Node<K,V> pred = e;
if ((e = e.next) == null) {
pred.next = new Node<K,V>(hash, key, value, null);
break;
}
}
}
else if (f instanceof TreeBin) { // 红黑树
Node<K,V> p;
binCount = 2;
if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key, value)) != null) {
oldVal = p.val;
if (!onlyIfAbsent)
p.val = value;
}
}
}
}
if (binCount != 0) {
if (binCount >= TREEIFY_THRESHOLD)
treeifyBin(tab, i);
if (oldVal != null)
return oldVal;
break;
}
}
}
addCount(1L, binCount); // 更新计数,可能触发扩容
return null;
}
get
public V get(Object key) {
Node<K,V>[] tab; Node<K,V> e, p; int n, h;
if (key == null) throw new NullPointerException();
h = spread(key.hashCode());
if ((tab = table) != null && (n = tab.length) > 0 &&
(e = tabAt(tab, (n - 1) & h)) != null) {
if (e.hash == h && ((k = e.key) == key || key.equals(k)))
return e.val;
if (e.hash >= 0) { // 链表
while (e != null) {
if (e.hash == h && ((k = e.key) == key || key.equals(k)))
return e.val;
e = e.next;
}
}
else if (e instanceof TreeBin) // 红黑树
return ((TreeBin<K,V>)e).getTreeNode(h, key).val;
else if (e.hash == MOVED) // 扩容中
return forwarderGet(e, key);
}
return null;
}
9. 性能分析
- 时间复杂度:
- 读操作:平均 O(1)(链表),最差 O(log n)(红黑树)。
- 写操作:平均 O(1)(单桶操作),扩容时 O(n)。
- 空间复杂度:O(n),n 为键值对数量。
- 并发性能:
- JDK 1.7:受限于 Segment 数量(默认 16)。
- JDK 1.8:桶级锁 + CAS,理论上支持更高并发。
10. 注意事项
- 键值非空:ConcurrentHashMap 不允许 null 键或值。
- 弱一致性:迭代器、size 等操作不保证实时一致。
- 扩容影响:高并发下频繁扩容可能导致性能抖动。
- 序列化:ConcurrentHashMap 支持序列化,但需注意并发状态可能不完全保存。
11. 总结
ConcurrentHashMap 是 Java 并发编程中高效、线程安全的哈希表实现。JDK 1.8 通过引入红黑树、细粒度锁、CAS 和并发扩容,显著提升了性能和可扩展性。其设计精髓在于 分而治之 和 无锁优化,适合高并发场景下的键值存储需求。理解其内部机制(如 CAS、桶级锁、红黑树转换)有助于开发者更好地使用和优化并发程序。
670

被折叠的 条评论
为什么被折叠?



