ConcurrentHashMap 原理详解
ConcurrentHashMap 是线程安全的 HashMap,JDK 1.8 从分段锁演进为 CAS+synchronized,性能大幅提升。
一、版本对比
1.1 核心差异
| 特性 | JDK 1.7 | JDK 1.8 |
|---|---|---|
| 底层结构 | Segment + HashEntry | Node + 链表 + 红黑树 |
| 锁机制 | 分段锁(ReentrantLock) | synchronized + CAS |
| 锁粒度 | Segment 级别 | 桶(Bucket)级别 |
| 并发度 | 16(Segment 数) | 数组长度(默认 16) |
| 读操作 | 无锁(volatile) | 无锁(volatile) |
1.2 演进原因
JDK 1.7 问题
├── 内存占用高(Segment 数组)
├── 并发度受限(固定 16)
└── 结构复杂
JDK 1.8 优化
├── 简化结构(移除 Segment)
├── 细粒度锁(桶级别)
└── 红黑树优化查询
二、JDK 1.7 实现
2.1 数据结构
ConcurrentHashMap
└── Segment[] segments (默认 16)
└── HashEntry[] table
└── HashEntry → HashEntry (链表)
// Segment 继承 ReentrantLock
static final class Segment<K,V> extends ReentrantLock {
transient volatile HashEntry<K,V>[] table;
}
2.2 初始化
// 初始化 Segment 数组
int sshift = 4; // 2^4 = 16
int segmentShift = 32 - sshift;
segments = new Segment[16];
// 每个 Segment 初始化 HashEntry 数组
for (int i = 0; i < segments.length; ++i)
segments[i] = new Segment<K,V>();
2.3 put 操作
public V put(K key, V value) {
// 1. 计算 hash
int hash = hash(key);
// 2. 定位 Segment
int j = (hash >>> segmentShift) & segmentMask;
Segment<K,V> segment = segments[j];
// 3. 锁定 Segment
segment.lock();
try {
// 4. 在 Segment 内执行 put
HashEntry<K,V>[] tab = segment.table;
// ... 插入逻辑
} finally {
segment.unlock();
}
}
2.4 优缺点
优点:
- 分段锁减少竞争
- 不同 Segment 可并发操作
缺点:
- 并发度受限(最多 16)
- 内存占用高
- 扩容慢
三、JDK 1.8 实现
3.1 数据结构
ConcurrentHashMap
└── Node[] table (数组)
├── Node → Node (链表)
└── TreeNode → TreeNode (红黑树)
// Node 结构
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
volatile V val; // volatile 保证可见性
volatile Node<K,V> next;
}
3.2 关键参数
// 默认容量
static final int DEFAULT_CAPACITY = 16;
// 树化阈值
static final int TREEIFY_THRESHOLD = 8;
// 非树化阈值
static final int UNTREEIFY_THRESHOLD = 6;
// 最小树化容量
static final int MIN_TREEIFY_CAPACITY = 64;
// 扩容阈值(负载因子 0.75)
static final int MAXIMUM_CAPACITY = 1 << 30;
3.3 put 操作
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;
// 1. 初始化数组
if (tab == null || (n = tab.length) == 0)
tab = initTable();
// 2. 桶为空,CAS 插入
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
if (casTabAt(tab, i, null,
new Node<K,V>(hash, key, value, null)))
break;
}
// 3. 扩容协助
else if ((fh = f.hash) == MOVED)
tab = helpTransfer(tab, f);
// 4. 桶非空,synchronized 锁头节点
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 = e.key;
if (ek.equals(key)) {
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;
}
3.4 锁优化
// synchronized 锁头节点(桶级别)
synchronized (f) {
// 插入/更新操作
}
// JDK 1.7 锁 Segment(段级别)
segment.lock();
优势:
- 锁粒度更细(桶 vs 段)
- 并发度更高(16 vs 数组长度)
- 内存占用更低
四、扩容机制
4.1 扩容触发
// 元素数量超过阈值
if (sizeCtl >= 0) {
if (tab == null || n >= sizeCtl)
transfer(tab, null);
}
4.2 多线程协助扩容
private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
int n = tab.length, stride;
// 计算每个线程处理的桶数
if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE)
stride = MIN_TRANSFER_STRIDE;
if (nextTab == null) {
// 创建新数组(容量翻倍)
nextTab = new Node<K,V>[n << 1];
}
// 多线程协助迁移
while (true) {
Node<K,V> f; int fh;
// CAS 获取迁移任务
while (nextIndex >= 0 || i + stride >= advanceIndex) {
// 迁移桶
if (f != null) {
// 链表/红黑树拆分
// 高位 → 新位置
// 低位 → 原位置
}
}
}
}
4.3 扩容优化
JDK 1.7:单线程扩容(Segment 内部)
JDK 1.8:多线程协助扩容(全局)
↓
多个线程同时参与迁移
CAS 标记迁移状态
避免重复迁移
五、读操作
5.1 get 方法
public V get(Object key) {
Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
int h = spread(key.hashCode());
// 1. 定位桶
if ((tab = table) != null && (n = tab.length) > 0 &&
(e = tabAt(tab, (n - 1) & h)) != null) {
// 2. 头节点匹配
if ((eh = e.hash) == h) {
if ((ek = e.key) == key || (ek != null && key.equals(ek)))
return e.val;
}
// 3. 遍历链表/红黑树
else if (eh < 0)
return (p = e.find(h, key)) != null ? p.val : null;
while ((e = e.next) != null) {
if (e.hash == h &&
((ek = e.key) == key || (ek != null && key.equals(ek))))
return e.val;
}
}
return null;
}
5.2 volatile 保证可见性
volatile V val; // 值可见
volatile Node<K,V> next; // 链表可见
无需加锁:读操作通过 volatile 保证可见性
六、使用注意
6.1 原子操作
ConcurrentHashMap<String, AtomicInteger> map = new ConcurrentHashMap<>();
// ❌ 非原子操作
map.get("count").incrementAndGet();
// ✅ 原子操作
map.computeIfAbsent("count", k -> new AtomicInteger()).incrementAndGet();
// ✅ 或使用 putIfAbsent
AtomicInteger count = new AtomicInteger();
map.putIfAbsent("count", count);
map.get("count").incrementAndGet();
6.2 批量操作
// 并行度阈值(默认 1)
long count = map.mappingCount();
// 批量遍历
map.forEach(10, (k, v) -> System.out.println(k + "=" + v));
// 批量搜索
Map.Entry<String, Integer> result = map.search(10, (k, v) -> {
if ("key".equals(k)) return new AbstractMap.SimpleEntry<>(k, v);
return null;
});
// 批量归约
String reduced = map.reduce(10, Map.Entry::getKey, (s1, s2) -> s1 + "," + s2);
6.3 与 Hashtable 对比
| 特性 | ConcurrentHashMap | Hashtable |
|---|---|---|
| 锁机制 | CAS + synchronized | synchronized(全表锁) |
| 并发度 | 高 | 低 |
| null 值 | 不支持 | 不支持 |
| 性能 | 高 | 低 |
| 推荐 | ✅ 是 | ❌ 否 |
七、总结
ConcurrentHashMap 核心要点:
| 版本 | 锁机制 | 并发度 | 性能 |
|---|---|---|---|
| JDK 1.7 | 分段锁 | 16 | 中 |
| JDK 1.8 | CAS + synchronized | 数组长度 | 高 |
演进优势:
- 简化结构(移除 Segment)
- 细粒度锁(桶级别)
- 红黑树优化(O(log n))
- 多线程协助扩容
JDK 1.8 的 ConcurrentHashMap 在几乎所有场景下都优于 1.7,是高并发首选。