ConcurrentHashMap 是 Java 并发包中提供的高性能线程安全的哈希表实现,其设计目标是支持高并发的读写操作。以下是其核心实现原理的详细分析:
分段锁(JDK 1.7 实现)
在 JDK 1.7 中,ConcurrentHashMap 使用分段锁(Segment)机制实现线程安全。哈希表被分为多个 Segment,每个 Segment 是一个独立的哈希表,拥有自己的锁。不同 Segment 的写操作可以并发执行,从而提高并发性能。
-
数据结构:
ConcurrentHashMap 由多个 Segment 组成,每个 Segment 是一个类似 HashMap 的结构,包含一个 HashEntry 数组。Segment 的数量通过并发级别(concurrencyLevel)指定,默认为 16。 -
锁粒度:
每个 Segment 继承自 ReentrantLock,写操作只需锁定当前 Segment,读操作无需加锁(通过 volatile 保证可见性)。 -
哈希冲突处理:
使用链地址法解决冲突,每个 HashEntry 是一个链表节点。
CAS + synchronized(JDK 1.8 实现)
JDK 1.8 对 ConcurrentHashMap 进行了优化,摒弃了分段锁,改用 CAS + synchronized 实现更细粒度的锁控制。
-
数据结构:
使用 Node 数组 + 链表/红黑树的结构(类似 HashMap)。当链表长度超过阈值(TREEIFY_THRESHOLD默认为 8)会尝试将链表转换为红黑树。仅当当前哈希表的容量大于或等于MIN_TREEIFY_CAPACITY(默认值为 64)时,链表才会真正转换为红黑树。如果容量不足,会优先触发扩容(resize)操作。当红黑树中的节点数量减少到UNTREEIFY_THRESHOLD(默认值为 6)时,会将红黑树转换回链表。 -
线程安全机制:
- 读操作:无锁,直接访问 volatile 的 Node 数组和节点数据。
- 写操作:
- 使用 CAS 初始化数组或插入节点。
- 发生冲突时,对链表头节点或树根节点加 synchronized 锁。
- 扩容:支持多线程协同扩容,通过 ForwardingNode 标记迁移状态。
关键优化点
-
减小锁粒度:
JDK 1.8 的锁仅针对单个桶(链表头或树根),比分段锁的粒度更细,进一步减少竞争。 -
红黑树优化:
链表过长时转换为红黑树,将查询时间复杂度从 O(n) 降为 O(log n)。 -
并发扩容:
扩容时允许其他线程协助迁移数据,避免单线程迁移的性能瓶颈。
示例代码片段
以下是 JDK 1.8 中 put 方法的简化逻辑:
final V putVal(K key, V value, boolean onlyIfAbsent) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
// 懒初始化数组
if ((tab = table) == null || (n = tab.length) == 0)
tab = initTable();
// CAS 插入节点
else if ((p = tabAt(tab, i = (n - 1) & hash)) == null) {
if (casTabAt(tab, i, null, new Node<K,V>(hash, key, value)))
break;
}
// 处理哈希冲突
else {
synchronized (p) {
if (tabAt(tab, i) == p) {
if (p instanceof TreeNode)
// 红黑树插入
((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
// 链表插入
for (int binCount = 0; ; ++binCount) {
if (p.next == null) {
p.next = new Node<K,V>(hash, key, value);
if (binCount >= TREEIFY_THRESHOLD - 1)
treeifyBin(tab, i);
break;
}
p = p.next;
}
}
}
}
}
addCount(1L, binCount);
return null;
}
性能对比
- JDK 1.7:分段锁适合中等并发场景,但在极高并发下仍可能因锁竞争受限。
- JDK 1.8:CAS + synchronized 在大多数场景下性能更高,尤其是读多写少时接近无锁性能。
适用场景
- 高并发读写需求(如缓存)。
- 需要线程安全且性能优于 Hashtable 或 Collections.synchronizedMap 的场景。
通过上述设计,ConcurrentHashMap 在保证线程安全的同时,极大提升了并发性能。
currentHashMap 扩容原理
CurrentHashMap 的扩容机制是为了在并发环境下高效地处理数据增长,同时保持线程安全性。以下是 JDK 8 中 CurrentHashMap 的扩容原理:
触发条件
扩容通常在以下情况下触发:
- 插入新元素时,发现当前链表或红黑树的节点数超过阈值(默认为 8,且 table 长度小于 64 时会优先扩容)。
- table 中元素数量超过容量 × 负载因子(默认 0.75)。
扩容过程
-
初始化新 table
创建一个新 table,大小为原 table 的 2 倍(即newCap = oldCap << 1),并计算新的扩容阈值(newThr = oldThr << 1)。 -
数据迁移
通过多线程协作完成迁移:- 每个线程负责迁移一部分桶(bucket),通过
transferIndex分配任务范围。 - 迁移时对原 table 的桶加锁(synchronized),保证线程安全。
- 每个线程负责迁移一部分桶(bucket),通过
-
桶迁移逻辑
- 对于链表:将原链表拆分为高位链和低位链,分别放入新 table 的
i和i + oldCap位置。 - 对于红黑树:调用
TreeNode.split()方法拆分为两棵树,若节点数小于等于 6 则退化为链表。
- 对于链表:将原链表拆分为高位链和低位链,分别放入新 table 的
-
更新引用
迁移完成后,将新 table 设置为当前 table,并更新阈值。
并发控制
- CAS 操作:用于更新
sizeCtl、transferIndex等全局变量。 - synchronized:迁移时对单个桶加锁,避免阻塞其他线程操作其他桶。
- 多线程协作:其他线程在插入时若发现正在扩容,会协助迁移。
示例代码片段
// JDK 8 扩容核心代码(简化版)
while (s-- > 0) {
synchronized (f) {
if (fh >= 0) {
// 处理链表
Node<K,V> ln = null, hn = null;
for (Node<K,V> e = f; e != null; e = e.next) {
int h = e.hash;
if ((h & oldCap) == 0) {
ln = new Node<K,V>(h, e.key, e.val, ln);
} else {
hn = new Node<K,V>(h, e.key, e.val, hn);
}
}
setTabAt(nextTab, i, ln);
setTabAt(nextTab, i + oldCap, hn);
}
// 处理红黑树...
}
}
关键点
- 高效性:多线程并行迁移减少总耗时。
- 无阻塞:读操作无需加锁,即使正在扩容也能正常访问。
- 渐进式:扩容期间新旧 table 共存,直到所有数据迁移完成。
为什么扩容使用2的幂次方?
高效计算桶索引:
使用2的幂大小时,模运算被位与操作取代,位与操作的计算成本更低。
例如,如果数组大小是16(2的4次方),表达式 hash & 15 可以高效地计算索引,等同于 hash % 16。
改善哈希码分布:
在位与操作中使用哈希码的低位有助于实现更均匀的键分布。
简化代码:
使用2的幂大小简化了计算桶索引的代码,使其更为简洁,潜在地更快速。
需要注意的是,虽然2的幂扩容提供了这些优势,但在某些情况下,它可能会导致哈希码的模式与2的幂大小对齐,从而产生更多的碰撞。然而,在实践中,简单性和性能之间的权衡通常倾向于使用2的幂扩容。
2330

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



