1. ConcurrentHashMap核心设计解析(JDK1.8版)
作为Java并发编程的核心组件,ConcurrentHashMap在JDK1.8中进行了彻底的重构。这次重构摒弃了分段锁的设计,转而采用更精细化的CAS+synchronized方案。我们先看其底层存储结构:
java复制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;
}
1.1 关键改进点对比
| 特性 | JDK1.7版本 | JDK1.8版本 |
|---|---|---|
| 锁粒度 | Segment分段锁 | 桶级别锁(头节点锁) |
| 数据结构 | 数组+链表 | 数组+链表/红黑树 |
| 并发控制 | ReentrantLock | CAS + synchronized |
| 扩容机制 | 分段扩容 | 协助扩容(多线程协同) |
重要提示:1.8版本的val和next字段都使用volatile修饰,保证可见性,但key和hash使用final确保不变性
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 并发控制实现细节
2.1 初始化阶段的线程安全
初始化table时采用CAS竞争机制:
java复制private final Node<K,V>[] initTable() {
while ((tab = table) == null || tab.length == 0) {
if ((sc = sizeCtl) < 0)
Thread.yield(); // 其他线程正在初始化
else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
try {
// 真正的初始化逻辑
} finally {
sizeCtl = sc;
}
}
}
}
2.2 put操作的三阶段锁策略
- 无锁阶段:通过hash计算桶位置,若桶为空则CAS插入
- 轻量锁阶段:hash冲突时对链表头节点加synchronized锁
- 重量锁阶段:链表转红黑树时的树节点锁
java复制final V putVal(K key, V value, boolean onlyIfAbsent) {
if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
if (casTabAt(tab, i, null, new Node<K,V>(hash, key, value)))
break; // CAS成功则插入完成
}
else {
synchronized (f) { // 锁住链表头节点
if (tabAt(tab, i) == f) {
// 链表插入或树节点处理
}
}
}
}
3. 扩容机制深度剖析
3.1 多线程协助扩容流程
扩容时会维护一个transferIndex变量标记当前扩容进度,线程可以通过这个变量领取自己的迁移任务区间:
code复制原数组: [0][1][2][3][4][5][6][7]
↑
transferIndex=4
迁移任务分配逻辑:
java复制if (U.compareAndSwapInt(this, TRANSFERINDEX, nextIndex,
nextBound = (nextIndex > stride ? nextIndex - stride : 0))) {
// 成功获取迁移区间[nextBound, nextIndex)
}
3.2 扩容期间的访问处理
- 未迁移桶:正常访问
- 迁移中桶:调用ForwardingNode的find方法转到新表查询
- 已迁移桶:直接访问新表
java复制static final class ForwardingNode<K,V> extends Node<K,V> {
final Node<K,V>[] nextTable;
ForwardingNode(Node<K,V>[] tab) {
super(MOVED, null, null);
this.nextTable = tab;
}
Node<K,V> find(int h, Object k) {
// 到新表中查找...
}
}
4. 红黑树转换机制
4.1 树化阈值与反树化
- 树化阈值:链表长度≥8且table.length≥64
- 反树化阈值:树节点数≤6时退化为链表
java复制private final void treeifyBin(Node<K,V>[] tab, int index) {
if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
resize(); // 优先扩容而非树化
else if ((e = tabAt(tab, index)) != null) {
// 真正的树化逻辑...
}
}
4.2 树节点的特殊处理
树节点继承自Node但使用不同的hash值:
java复制static final class TreeNode<K,V> extends Node<K,V> {
TreeNode<K,V> parent; // 红黑树父节点
TreeNode<K,V> left;
TreeNode<K,V> right;
TreeNode<K,V> prev; // 保留链表指针
TreeNode(int hash, K key, V val, Node<K,V> next) {
super(hash, key, val, next);
}
}
5. 关键性能优化技巧
5.1 避免伪共享的设计
通过@sun.misc.Contended注解防止缓存行竞争:
java复制@sun.misc.Contended
static final class CounterCell {
volatile long value;
CounterCell(long x) { value = x; }
}
5.2 size计算的优化方案
采用分片计数避免竞争:
java复制final long sumCount() {
CounterCell[] as = counterCells;
long sum = baseCount;
if (as != null) {
for (CounterCell a : as)
if (a != null)
sum += a.value;
}
return sum;
}
6. 实际应用中的避坑指南
- 死循环风险:在JDK1.8中已解决,但在迭代时若发生扩容仍需注意
- 内存可见性:computeIfAbsent等方法中的lambda应保持简单
- hash冲突攻击:合理实现hashCode()方法
经验之谈:在computeIfAbsent中执行耗时操作可能导致锁持有时间过长
7. 与其它并发容器的对比
| 特性 | ConcurrentHashMap | Hashtable | Collections.synchronizedMap |
|---|---|---|---|
| 锁粒度 | 桶级别 | 全表锁 | 全表锁 |
| 迭代器 | 弱一致性 | 强一致性 | 强一致性 |
| null值支持 | 不允许 | 不允许 | 允许 |
| 并发性能 | 最优 | 差 | 差 |
8. 源码调试技巧
- 使用-XX:+PrintConcurrentLocks查看锁竞争情况
- 通过jstack观察线程在transfer方法中的状态
- 使用JOL工具分析对象内存布局
bash复制# 使用JOL分析内存布局示例
java -jar jol-cli.jar internals java.util.concurrent.ConcurrentHashMap$Node
9. 典型应用场景
- 全局缓存实现
- 实时计数器
- 会话管理
- 事件总线注册表
java复制// 线程安全的计数器示例
ConcurrentHashMap<String, LongAdder> counters = new ConcurrentHashMap<>();
counters.computeIfAbsent(key, k -> new LongAdder()).increment();
10. 扩展思考
-
为什么选择synchronized而不是ReentrantLock?
- JVM对synchronized的持续优化
- 锁粒度足够细时性能差异小
- 减少内存开销
-
为什么链表长度达到8才树化?
- 基于泊松分布的概率计算(链表长度≥8的概率小于千万分之一)
-
如何实现弱一致性的迭代器?
- 遍历时使用当前快照
- 不抛出ConcurrentModificationException
