ConcurrentHashMap通过CAS和细粒度锁(JDK8+)实现高并发线程安全,支持高效读写与动态扩容,适用于缓存、计数器等高并发场景,相比Hashtable性能更优。
一、核心特性
- 线程安全:高并发场景下性能接近非同步的
HashMap。 - 分段锁(JDK7) :将数据分段(
Segment),每个段独立加锁。 - CAS +
synchronized(JDK8+) :细粒度锁(桶级别)和无锁化操作。 - 支持高并发读写:读操作通常无锁,写操作锁粒度细化。
- 不允许
null键或值:避免歧义(如get(key)返回null时无法区分“不存在”或“值为null”)。
二、底层数据结构演进
| JDK版本 | 实现方式 | 锁机制 | 性能优化 |
|---|---|---|---|
| JDK7 | 分段锁(Segment 数组) | 每个 Segment 继承 ReentrantLock | 段级别锁,减少竞争 |
| JDK8+ | 数组 + 链表/红黑树 + CAS | synchronized 锁桶头节点 + CAS | 锁粒度更细,并发度更高 |
三、JDK8+ 核心实现原理
1. 数据结构
-
数组:
Node<K,V>[] table,每个桶存储链表或红黑树。 -
链表节点:
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; // 链表指针 } -
红黑树节点:
static final class TreeNode<K,V> extends Node<K,V> { TreeNode<K,V> parent; // 父节点 TreeNode<K,V> left; // 左子树 TreeNode<K,V> right; // 右子树 boolean red; // 颜色标记 }
2. 关键机制
-
CAS(Compare-And-Swap) :用于无锁化初始化、节点插入等操作。
// 使用 Unsafe 类实现 CAS static final <K,V> boolean casTabAt(Node<K,V>[] tab, int i, Node<K,V> c, Node<K,V> v) { return U.compareAndSetReference(tab, ((long)i << ASHIFT) + ABASE, c, v); } -
锁细化:写操作仅锁定当前桶的头节点(链表或红黑树)。
-
多线程协助扩容:线程在操作时发现正在扩容,会协助迁移数据。
四、核心操作流程
1. put() 流程
ThreadConcurrentHashMapput(key, value)计算 key 的 hash定位桶下标CAS 插入新节点协助扩容synchronized 锁住头节点遍历链表/红黑树更新值插入新节点树化alt[链表长度≥8]alt[Key 存在]alt[桶正在迁移(Forw-ardingNode)]alt[桶为空][桶非空]检查是否需要扩容返回旧值(或 null)ThreadConcurrentHashMap
2. get() 流程
- 无锁读取:直接访问
volatile字段,保证可见性。 - 处理扩容:若遇到
ForwardingNode,转到新数组查找。
3. 扩容机制(transfer())
-
触发条件:元素数量超过
sizeCtl阈值。 -
多线程协作:
- 每个线程处理一个步长(默认 16)的桶区间。
- 通过
ForwardingNode标记正在迁移的桶。
-
迁移规则:
// 例如旧容量为 16,新容量为 32 if ((e.hash & oldCap) == 0) { // 新索引 = 原位置 } else { // 新索引 = 原位置 + oldCap }
五、线程安全实现对比
| 实现方式 | 锁粒度 | 并发性能 | 适用场景 |
|---|---|---|---|
| Hashtable | 全局锁(方法级) | 低 | 低并发遗留代码 |
| Collections.synchronizedMap | 全局锁(对象级) | 中 | 简单同步需求 |
| ConcurrentHashMap(JDK7) | 段级别锁 | 高 | 中等并发读写 |
| ConcurrentHashMap(JDK8+) | 桶级别锁 | 极高 | 高并发读写、大规模数据场景 |
六、关键源码解析(JDK17)
1. 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(); // CAS 初始化数组
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
if (casTabAt(tab, i, null, new Node<K,V>(hash, key, value)))
break; // CAS 插入成功
}
else if ((fh = f.hash) == MOVED)
tab = helpTransfer(tab, f); // 协助扩容
else {
synchronized (f) { // 锁住头节点
// 处理链表或红黑树插入
}
}
}
addCount(1L, binCount); // 更新计数,检查扩容
return null;
}
2. size() 实现
-
基础计数:使用
LongAdder思想,通过CounterCell[]分段计数。 -
最终一致性:
size()返回近似值(非精确值),适合高并发场景。public int size() { long n = sumCount(); return ((n < 0L) ? 0 : (n > (long)Integer.MAX_VALUE) ? Integer.MAX_VALUE : (int)n); }
七、适用场景与最佳实践
1. 推荐场景
-
高并发计数器:
ConcurrentHashMap<String, Long> counter = new ConcurrentHashMap<>(); counter.compute(key, (k, v) -> (v == null) ? 1 : v + 1); -
缓存系统:如商品信息缓存,支持高频读取和低频更新。
-
实时数据处理:多线程写入事件数据,保证线程安全。
2. 避免场景
- 需要强一致性的
size():使用AtomicLong替代。 - 频繁全表扫描:迭代器弱一致性,可能遗漏或重复数据。
3. 性能优化技巧
-
初始化容量:避免扩容开销。
new ConcurrentHashMap<>(32); // 初始容量32 -
避免长链表:优化键的哈希函数,减少冲突。
-
合理选择并发级别(JDK7):
new ConcurrentHashMap(16, 0.75f, 8); // 初始容量16,并发级别8
八、与其他并发容器对比
| 容器 | 线程安全实现 | 有序性 | 适用场景 |
|---|---|---|---|
| ConcurrentHashMap | 桶锁 + CAS | 无序 | 高频键值操作 |
| ConcurrentSkipListMap | 跳表(无锁读,CAS写) | 有序 | 需要排序或范围查询 |
| Hashtable | 全局锁 | 无序 | 遗留代码 |
| Collections.synchronizedMap | 对象锁 | 无序 | 简单同步需求 |
九、总结
- 核心优势:通过分段锁(JDK7)和 CAS +
synchronized(JDK8+)实现高并发读写。 - 数据结构:数组 + 链表/红黑树,支持动态扩容与树化优化。
- 适用场景:高并发键值存储、计数器、缓存系统。
- 最佳实践:合理初始化容量、优化哈希函数、避免锁竞争热点。