Java 并发容器源码解析 ConcurrentHashMap 1.8 分段锁优化
Java并发容器深度解析:ConcurrentHashMap 1.8的分段锁优化与实现原理
本文基于最新的Java版本,深入分析ConcurrentHashMap在JDK 1.8中的重大改进,带你理解如何通过CAS+synchronized实现高性能线程安全。
1. ConcurrentHashMap的重要演进
在Java并发编程中,ConcurrentHashMap 是一个不可或缺的高性能并发容器。从JDK 1.5引入至今,它经历了重大的架构演变。JDK 1.7及之前采用分段锁技术,而JDK 1.8则进行了彻底的重构,放弃了分段锁,改用CAS + synchronized 来实现更细粒度的锁优化。
2. JDK 1.8之前的分段锁机制
在深入1.8版本的优化之前,我们先简要回顾一下分段锁的设计思想。JDK 1.7中的ConcurrentHashMap使用了一个Segment数组,每个Segment本质上是一个小的HashMap,独立加锁。
java
// JDK 1.7中的分段锁实现
final Segment<K,V>[] segments;
static final class Segment<K,V> extends ReentrantLock {
// 每个Segment独立管理一部分数据
}
分段锁的优势在于不同的段可以并发操作,减少了锁竞争。但当需要跨段操作(如全局的size()计算)时,性能会受到影响。
3. JDK 1.8的架构革命:抛弃分段锁
JDK 1.8对ConcurrentHashMap进行了彻底重构,最重要的变化是放弃了分段锁,改为使用:
- CAS(Compare-And-Swap)无锁算法
- synchronized关键字实现细粒度锁
- 节点级锁,只锁定当前操作的桶(bucket)
3.1 核心数据结构变化
```java
// JDK 1.8的核心数据结构
transient volatile Node[] table;
private transient volatile int sizeCtl;
static class Node implements Map.Entry {
final int hash;
final K key;
volatile V val;
volatile Node next;
}
```
最大的变化是直接用Node数组替代了Segment数组,每个桶(bucket)独立管理,实现了真正的细粒度锁。
4. CAS + synchronized的实现原理
4.1 无锁化的CAS操作
在put操作中,首先尝试使用CAS无锁化方式插入新节点:
java
// 简化的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) {
// 关键点1:使用CAS无锁化添加新节点
if (casTabAt(tab, i, null, new Node<K,V>(hash, key, value, null)))
break;
}
// ... 其他情况处理
}
addCount(1L, binCount);
return null;
}
4.2 synchronized精细化锁
当CAS失败(说明有竞争)时,对当前桶的第一个节点加锁:
java
// 桶不为空时的处理
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 || (ek != null && 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) {
// 红黑树处理逻辑
}
}
}
锁粒度细化:从分段锁(锁定一个Segment包含多个桶)变为节点锁(只锁定单个桶),大幅减少了锁竞争。
5. 扩容机制的优化
JDK 1.8在扩容时引入了多线程协同扩容机制,进一步提升了性能:
5.1 扩容触发条件
java
// 扩容判断逻辑
if (check >= 0) {
Node<K,V>[] tab, nt; int n, sc;
while (s >= (long)(sc = sizeCtl) && (tab = table) != null && (n = tab.length) < MAXIMUM_CAPACITY) {
int rs = resizeStamp(n);
if (sc < 0) {
// 其他线程正在扩容,协助扩容
if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 || sc == rs + MAX_RESIZERS || (nt = nextTable) == null || transferIndex <= 0)
break;
if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1))
transfer(tab, nt);
}
else if (U.compareAndSwapInt(this, SIZECTL, sc, (rs << RESIZE_STAMP_SHIFT) + 2))
transfer(tab, null); // 发起扩容
s = sumCount();
}
}
5.2 数据迁移策略
扩容时采用步长控制,每个线程负责一部分桶的迁移,实现了真正的并行扩容:
```java
// 数据迁移的核心逻辑
if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE)
stride = MIN_TRANSFER_STRIDE; // 计算每个线程处理的步长
// 每个线程处理自己范围内的桶
while (advance) {
int nextIndex, nextBound;
if (--i >= bound || finishing)
advance = false;
else if ((nextIndex = transferIndex) <= 0) {
i = -1;
advance = false;
}
else if (U.compareAndSwapInt(this, TRANSFERINDEX, nextIndex, nextBound = (nextIndex > stride ? nextIndex - stride : 0))) {
bound = nextBound;
i = nextIndex - 1;
advance = false;
}
}
```
6. 红黑树优化处理
当链表长度超过阈值(默认为8)时,转换为红黑树;当节点数小于6时,退化为链表。这种设计既保证了查询效率,又避免了过度复杂化。
java
// 树化阈值
static final int TREEIFY_THRESHOLD = 8;
// 链化阈值
static final int UNTREEIFY_THRESHOLD = 6;
7. 性能对比与实战建议
7.1 性能优势
- 锁粒度更细:从段级别到桶级别,竞争概率大幅降低
- CAS无锁化:在无竞争情况下性能接近HashMap
- 扩容并行化:多线程协同扩容,避免单点瓶颈
7.2 使用建议
- 适合场景:高并发读写、缓存实现、计数器等
- 注意事项:虽然线程安全,但复合操作仍需外部同步
- size()方法:采用分段计数,是近似值而非精确值
8. 总结
JDK 1.8中ConcurrentHashMap的分段锁优化代表了Java并发编程的重要进步。通过CAS + synchronized的组合,实现了比分段锁更细粒度的锁控制,同时在扩容机制、数据结构等方面都进行了深度优化。这种设计思想不仅提升了性能,也为我们在实际开发中处理高并发场景提供了宝贵的参考。
理解ConcurrentHashMap的底层实现,不仅有助于我们更好地使用这个工具,更能深入理解Java并发编程的精髓,为设计高性能、高并发的系统打下坚实基础。
参考文档:Oracle官方JDK文档、Java并发编程实战、最新OpenJDK源码分析
更多推荐


所有评论(0)