首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >【集合框架ConcurrentHashMap进阶】

【集合框架ConcurrentHashMap进阶】

作者头像
艾伦耶格尔
发布2025-08-28 15:56:57
发布2025-08-28 15:56:57
3420
举报
文章被收录于专栏:Java基础Java基础

一、ConcurrentHashMap:从源码看高并发的智慧

在 Java 并发编程中,ConcurrentHashMap 是当之无愧的“明星类”。 它既不像 HashMap 那样“裸奔”,也不像 Hashtable 那样“一锁锁全表”。 它通过精巧的设计,在线程安全高性能之间找到了完美的平衡。 本文将带你深入源码,彻底搞懂它的底层实现、扩容机制、锁的使用方式,并对比 JDK 1.7 与 JDK 1.8 的演进与差异。


1. 核心目标:线程安全 + 高并发性能

ConcurrentHashMap 的设计目标非常明确:

线程安全:多线程读写不出现数据错乱 ✅ 高并发性能:支持多个线程同时读写,互不阻塞 ✅ 可伸缩性:在多核 CPU 下性能随核心数线性增长

为了实现这些目标,它在不同 JDK 版本中采用了完全不同的策略。


二、JDK 1.7:分段锁(Segment)时代

1. 整体结构

JDK 1.7 的 ConcurrentHashMap 采用了 “分段锁”(Segment) 的设计思想:

  • 将整个 map 分成多个 Segment(默认 16 个)
  • 每个 Segment 是一个独立的 ReentrantLock,内部是一个 HashEntry 数组
  • 每个 Segment 管理一部分 key,互不干扰
代码语言:javascript
复制
ConcurrentHashMap map = new ConcurrentHashMap();
// 默认 concurrencyLevel = 16
结构图示意:
代码语言:javascript
复制
ConcurrentHashMap
├── Segment[0] → HashEntry[...](锁0)
├── Segment[1] → HashEntry[...](锁1)
├── ...
└── Segment[15] → HashEntry[...](锁15)

一个线程访问 Segment[0],另一个线程可以同时访问 Segment[1],实现并发写


2. 锁的使用:Segment 继承 ReentrantLock

  • 每个 Segment 继承自 ReentrantLock,可以独立加锁。
  • 写操作时,先定位到 Segment,然后调用 lock()
  • 读操作使用 volatile 保证可见性,无需加锁
代码语言:javascript
复制
final Segment<K,V> s = segmentForHash(hash);
s.lock();
try {
    // 执行 put/remove
} finally {
    s.unlock();
}

✅ 优点:并发度高(默认支持 16 个线程并发写) ❌ 缺点:结构复杂,Segment 数量固定,不够灵活


3. 扩容机制:Segment 内部独立扩容

  • 每个 Segment 内部维护自己的 HashEntry 数组。
  • 当某个 Segment 的元素过多时,只扩容它自己,不影响其他 Segment
  • 扩容是单线程进行的,不会协助。

缺点:扩容粒度小,但无法利用多核并行优势。


三、JDK 1.8:CAS + synchronized 桶位锁时代

JDK 1.8 彻底重构了 ConcurrentHashMap,放弃了 Segment,回归数组 + 链表/红黑树结构,借鉴了 HashMap 的优化思路,但加入了并发控制。


1. 整体结构

代码语言:javascript
复制
transient volatile Node<K,V>[] table;
  • table 是一个 Node 数组,每个 Node 是链表节点。
  • 当链表长度 ≥ 8 且 table.length >= 64 时,链表转为红黑树(TreeNode)。
  • 使用 volatile 保证数组的可见性。
结构图示意:
代码语言:javascript
复制
table[0] → Node → Node → ...(可能转为 TreeNode)
table[1] → Node
...
table[n-1] → null

✅ 优点:结构简单,与 HashMap 一致,易于维护和优化。


2. 锁的使用:synchronized 桶位锁

这是 JDK 1.8 最大的变化之一:synchronized 替代 ReentrantLock,锁粒度更小

🔐 锁的是什么?
  • 写操作时,对数组桶位的头节点加 synchronized 锁。
  • 也就是说,只有哈希冲突的链表/树才会被锁定。
  • 不同桶位的写操作完全并发。
代码语言:javascript
复制
Node<K,V> f = tabAt(tab, i = (n - 1) & hash); // 定位桶
synchronized (f) { // 只锁这个桶的头节点
    if (tabAt(tab, i) == f) {
        // 插入或更新
    }
}
为什么用 synchronized?
  • JDK 1.6 以后,synchronized 经过优化(偏向锁、轻量级锁、自旋),性能已不输 ReentrantLock
  • synchronized 更轻量,JVM 层面支持,代码更简洁。

✅ 优势:锁粒度极小,并发性能极高。


3. CAS 的广泛应用

CAS(Compare and Swap)是 JDK 1.8 中实现无锁化操作的核心。

CAS 用在哪些地方?

场景

CAS 作用

初始化 table

只有一个线程能成功 CAS 设置 sizeCtl,避免重复初始化

插入第一个节点

若桶为空,用 CAS 插入,避免竞争

修改控制变量

如 sizeCtl、transferIndex 等

代码语言:javascript
复制
// 尝试初始化 table
if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
    // 成功的线程进入初始化
}

✅ 优势:无锁,性能高,适合低竞争场景。


四、扩容原理:多线程协助扩容(Transfer)

这是 JDK 1.8 最精妙的设计之一:扩容不再是一个线程的苦力活,而是多线程协作完成


1. 扩容触发条件

  • put 操作后,检查 size 是否超过阈值(0.75 * capacity
  • 如果超过,调用 tryPresize 或 helpTransfer 触发扩容

2. 扩容核心:多线程协助(Helping Transfer)

关键字段:
代码语言:javascript
复制
private transient volatile int sizeCtl;
private transient volatile int transferIndex; // 下一个迁移任务的起始位置
扩容流程:
  1. 一个线程发起扩容,将 sizeCtl 设为 -1,表示“正在初始化”
  2. 计算新容量 newCap = oldCap << 1
  3. 创建 nextTable(新数组)
  4. 将 transferIndex 设为 newTable.length
  5. 多个线程通过 transferIndex 领取迁移任务,各自负责一段桶位的迁移
迁移过程:
  • 每个线程从 transferIndex 向前领取一段桶位(如 16 个)
  • 将旧 table[i] 的节点迁移到 nextTable
  • 迁移完成后,在旧位置放置 ForwardingNode,表示“该桶已迁移”
代码语言:javascript
复制
// 伪代码
while (transferIndex > 0) {
    int idx = transferIndex - 1;
    transferIndex = idx;
    // 迁移 table[idx]
}

3. ForwardingNode:迁移的“路标”

  • ForwardingNode 是一个特殊的节点,hash = MOVED (-1)
  • 它不存储数据,只指向 nextTable
  • 当线程访问一个桶时,发现是 ForwardingNode,就会自动协助迁移或直接去 nextTable 查找
代码语言:javascript
复制
if (f.hash == MOVED) {
    // 表示正在扩容,协助迁移
    tab = helpTransfer(tab, f);
}

✅ 优势:读写线程都能参与扩容,极大提升扩容效率。


五、JDK 1.7 与 JDK 1.8 的核心异同对比

特性

JDK 1.7

JDK 1.8

锁机制

Segment 继承 ReentrantLock

synchronized 锁桶位头节点

锁粒度

Segment 级(默认 16 个)

桶位级(每个 Node 一个锁)

CAS 使用

较少

广泛用于初始化、插入、控制变量

数据结构

Segment + HashEntry

Node + 链表/红黑树

扩容机制

单线程扩容,Segment 独立

多线程协助扩容,全局迁移

volatile 使用

有限

table、sizeCtl 等广泛使用

性能

较高,但结构复杂

更高,结构简单,适合多核

✅ 总结:JDK 1.8 的设计更简洁、更高效,充分利用了 CAS 和多线程协作,是现代并发编程的典范。


六、为什么不允许 null 键和值?

代码语言:javascript
复制
if (key == null || value == null) throw new NullPointerException();

原因很简单:避免歧义

  • get(key) 返回 null,可能是“键不存在”,也可能是“值为 null”
  • 在并发环境下,这种歧义会导致逻辑错误

HashMap 允许 null,是因为它是单线程使用的,开发者可以自行判断。


七、总结

ConcurrentHashMap 从 JDK 1.7 到 JDK 1.8 的演进,是一次从“分而治之”到“无锁化 + 细粒度锁”的思想跃迁

版本

核心思想

关键技术

JDK 1.7

分段锁

Segment + ReentrantLock

JDK 1.8

无锁化 + 桶位锁

CAS + synchronized + volatile + 多线程扩容

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2025-08-27,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一、ConcurrentHashMap:从源码看高并发的智慧
    • 1. 核心目标:线程安全 + 高并发性能
  • 二、JDK 1.7:分段锁(Segment)时代
    • 1. 整体结构
      • 结构图示意:
    • 2. 锁的使用:Segment 继承 ReentrantLock
    • 3. 扩容机制:Segment 内部独立扩容
  • 三、JDK 1.8:CAS + synchronized 桶位锁时代
    • 1. 整体结构
      • 结构图示意:
    • 2. 锁的使用:synchronized 桶位锁
      • 🔐 锁的是什么?
      • 为什么用 synchronized?
    • 3. CAS 的广泛应用
      • CAS 用在哪些地方?
  • 四、扩容原理:多线程协助扩容(Transfer)
    • 1. 扩容触发条件
    • 2. 扩容核心:多线程协助(Helping Transfer)
      • 关键字段:
      • 扩容流程:
      • 迁移过程:
    • 3. ForwardingNode:迁移的“路标”
  • 五、JDK 1.7 与 JDK 1.8 的核心异同对比
  • 六、为什么不允许 null 键和值?
  • 七、总结
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档