0%

ConcurrentHashMap详解

ConcurrentHashMap 深度解析(基于 JDK 8)

ConcurrentHashMap 是 Java 并发包(java.util.concurrent)中提供的线程安全的 Map 实现,专为高并发场景设计。它解决了传统 Hashtable 全表锁效率低下的问题,通过细粒度的同步机制(JDK 8 中为 CAS + synchronized)实现高效并发,是多线程环境下键值对存储的首选。

ConcurrentHashMap 核心特性

线程安全

通过细粒度同步机制(而非全表锁)保证多线程读写安全,不同桶(数组索引)的操作可并发执行,大幅提升并发效率。

高效并发

  • 读操作(get)无锁(依赖 volatile 保证可见性),性能接近 HashMap
  • 写操作(putremove)仅锁定哈希冲突的链表 / 红黑树头节点,不阻塞其他桶的操作。

不允许 null 键值

键(key)和值(value)均不能为 null,避免与 get 方法返回 null 的歧义(无法区分 “键不存在” 和 “值为 null”)。

支持原子操作

提供 putIfAbsentremove(带条件)、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>:基础节点,存储键值对,valuevolatile 修饰,保证多线程可见性。

    1
    2
    3
    4
    5
    6
    7
    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>:扩容时的转发节点,标记当前桶已迁移至新数组,引导线程协助扩容。

核心变量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 哈希表数组(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 实现并发插入,核心逻辑如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
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 保证节点值的可见性:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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)时触发扩容,容量翻倍,过程支持多线程协作:

  1. 初始化新数组:通过 CAS 将 sizeCtl 设为负值(标记扩容中),创建新数组(容量为原数组的 2 倍)。
  2. 迁移数据:原数组中的每个桶由一个线程负责迁移,通过 synchronized 锁定桶的头节点,避免并发修改。
  3. 协助扩容:其他线程发现扩容标记(ForwardingNode)时,会主动参与迁移,加速过程。
  4. 替换数组:迁移完成后,用新数组替换原数组,更新 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(全表)
适用场景 高并发读写 单线程 / 无并发 旧系统兼容

欢迎关注我的其它发布渠道