ConcurrentHashMap详解
ConcurrentHashMap 深度解析(基于 JDK 8)
ConcurrentHashMap 是 Java 并发包(java.util.concurrent)中提供的线程安全的 Map 实现,专为高并发场景设计。它解决了传统 Hashtable 全表锁效率低下的问题,通过细粒度的同步机制(JDK 8 中为 CAS + synchronized)实现高效并发,是多线程环境下键值对存储的首选。
ConcurrentHashMap 核心特性
线程安全
通过细粒度同步机制(而非全表锁)保证多线程读写安全,不同桶(数组索引)的操作可并发执行,大幅提升并发效率。
高效并发
- 读操作(
get)无锁(依赖volatile保证可见性),性能接近HashMap。 - 写操作(
put、remove)仅锁定哈希冲突的链表 / 红黑树头节点,不阻塞其他桶的操作。
不允许 null 键值
键(key)和值(value)均不能为 null,避免与 get 方法返回 null 的歧义(无法区分 “键不存在” 和 “值为 null”)。
支持原子操作
提供 putIfAbsent、remove(带条件)、replace 等原子操作,无需额外同步即可实现线程安全的复合逻辑(如 “不存在则插入”“存在则更新”)。
弱一致性迭代器
迭代器遍历期间允许其他线程修改集合,不会抛出 ConcurrentModificationException(弱一致性),但可能无法实时反映最新修改。
底层结构与并发机制演变
ConcurrentHashMap 的底层结构随 JDK 版本演进,核心差异在于锁机制:
JDK 7:分段锁(Segment)
- 结构:整体为
Segment数组(默认 16 个),每个Segment是一个独立的哈希表(类似HashMap),包含HashEntry数组 + 链表。 - 锁机制:每个
Segment独立加锁(ReentrantLock),不同Segment的操作可并发执行,锁粒度为 “段”。 - 局限:段数固定(默认 16),并发度受限;扩容仅针对单个
Segment,复杂且效率较低。
JDK 8:CAS + synchronized(主流版本)
彻底废弃 Segment,采用与 HashMap 类似的 “数组 + 链表 / 红黑树” 结构,但通过更细粒度的同步实现并发安全:
- 数组:
Node<K,V>[] table,存储键值对,每个元素为链表 / 红黑树的头节点。 - 链表 / 红黑树:解决哈希冲突,链表长度超过 8 且数组容量 ≥ 64 时转为红黑树。
- 锁机制:
- 对空桶(索引位置为
null)使用 CAS 原子操作插入节点; - 对非空桶(存在哈希冲突)使用
synchronized锁定头节点,仅阻塞同桶的并发操作。
- 对空桶(索引位置为
核心结构:节点与数组
核心节点类型
JDK 8 中通过不同类型的节点实现并发控制和数据存储:
Node<K,V>:基础节点,存储键值对,value用volatile修饰,保证多线程可见性。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; // 链表后继节点,volatile 保证链表结构可见性 // ... 构造器和方法 }TreeNode<K,V>:红黑树节点,继承Node,增加树结构的父子节点引用,用于哈希冲突严重时提升查询效率。ForwardingNode<K,V>:扩容时的转发节点,标记当前桶已迁移至新数组,引导线程协助扩容。
核心变量
// 哈希表数组(volatile 修饰,保证扩容后可见性)
transient volatile Node<K,V>[] table;
// 扩容时的新数组(volatile 修饰)
private transient volatile Node<K,V>[] nextTable;
// 元素数量(通过 CAS 原子更新)
private transient volatile long baseCount;
// 控制扩容的标记(负值表示正在扩容,正值为下次扩容阈值)
private transient volatile int sizeCtl;
// 加载因子(默认 0.75,与 HashMap 一致)
private static final float LOAD_FACTOR = 0.75f;
核心方法解析(JDK 8)
1. put(K key, V value):线程安全插入
put 方法通过 CAS 和 synchronized 实现并发插入,核心逻辑如下:
public V put(K key, V value) {
return putVal(key, value, false);
}
final V putVal(K key, V value, boolean onlyIfAbsent) {
if (key == null || value == null) throw new NullPointerException(); // 禁止 null 键值
int hash = spread(key.hashCode()); // 二次哈希(与 HashMap 类似,增强分布均匀性)
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 保证线程安全)
}
// 1. 计算索引,若桶为空,用 CAS 插入新节点
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
if (casTabAt(tab, i, null, new Node<>(hash, key, value, null))) {
break; // CAS 成功,插入完成
}
}
// 2. 若桶正在扩容(头节点为 ForwardingNode),协助扩容
else if ((fh = f.hash) == MOVED) {
tab = helpTransfer(tab, f);
}
// 3. 桶非空且未扩容,用 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;
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<>(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;
}
并发控制关键点:
- 空桶插入用 CAS 避免加锁,提升效率;
- 非空桶用
synchronized锁定头节点,仅阻塞同桶的并发写入,不同桶可并行; - 扩容时通过
ForwardingNode引导其他线程协助扩容,加速扩容过程。
2. get(Object key):无锁读操作
读操作全程无锁,依赖 volatile 保证节点值的可见性:
public V get(Object key) {
Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
int hash = spread(key.hashCode()); // 计算哈希值
if ((tab = table) != null && (n = tab.length) > 0 &&
(e = tabAt(tab, (n - 1) & hash)) != null) { // 定位桶(volatile 读)
if ((eh = e.hash) == hash) { // 头节点匹配
if ((ek = e.key) == key || (ek != null && key.equals(ek))) {
return e.val;
}
}
else if (eh < 0) { // 头节点为红黑树或扩容转发节点
return (p = e.find(hash, key)) != null ? p.val : null;
}
while ((e = e.next) != null) { // 遍历链表
if (e.hash == hash && ((ek = e.key) == key || (ek != null && key.equals(ek)))) {
return e.val;
}
}
}
return null;
}
无锁原理:
- 数组
table和节点val均用volatile修饰,保证修改后对其他线程可见; - 读操作无需加锁,仅通过
volatile语义获取最新值,性能接近HashMap。
3. 扩容机制(transfer)
当元素数量超过阈值(capacity * loadFactor)时触发扩容,容量翻倍,过程支持多线程协作:
- 初始化新数组:通过 CAS 将
sizeCtl设为负值(标记扩容中),创建新数组(容量为原数组的 2 倍)。 - 迁移数据:原数组中的每个桶由一个线程负责迁移,通过
synchronized锁定桶的头节点,避免并发修改。 - 协助扩容:其他线程发现扩容标记(
ForwardingNode)时,会主动参与迁移,加速过程。 - 替换数组:迁移完成后,用新数组替换原数组,更新
sizeCtl为新阈值。
原子操作与典型应用
1. 原子操作(ConcurrentMap 接口定义)
putIfAbsent(K key, V value):若键不存在则插入,返回旧值(null表示插入成功),常用于 “初始化唯一实例” 场景(如缓存加载)。remove(Object key, Object value):仅当键关联的值与给定值匹配时才删除,返回是否成功。replace(K key, V oldVal, V newVal):仅当键关联的值为oldVal时才更新为newVal,返回是否成功。
2. 典型应用场景
- 高并发缓存:如服务器本地缓存,多线程读写共享数据(如用户会话、配置信息)。
- 共享计数器:用
putIfAbsent初始化计数器,replace原子更新计数。 - 避免重复计算:通过
putIfAbsent保证同一键对应的 value 仅被计算一次(如懒加载场景)。
与其他 Map 的对比
| 特性 | ConcurrentHashMap | HashMap | Hashtable |
|---|---|---|---|
| 线程安全 | 是(细粒度锁) | 否 | 是(全表锁) |
| 并发效率 | 高(读写可并发) | 无并发控制 | 低(全表锁阻塞) |
| null 键值 | 不允许 | 允许(key 最多 1 个) | 不允许 |
| 底层结构(JDK 8) | 数组 + 链表 / 红黑树 | 数组 + 链表 / 红黑树 | 数组 + 链表 |
| 锁机制 | CAS + synchronized |
无 | synchronized(全表) |
| 适用场景 | 高并发读写 | 单线程 / 无并发 | 旧系统兼容 |