ConcurrentHashMap
是 Java 提供的一种高效、线程安全的哈希表实现,主要用于多线程环境下替代传统的 HashMap
和 Hashtable
。
1. 基本概念
- 所在包:
java.util.concurrent
- 线程安全: 是(高效)
- 是否支持 null 键/值: 不支持(会抛出
NullPointerException
) - JDK 1.8 中采用锁分离 + CAS + 红黑树优化方案
2. 数据结构
JDK 1.8 中 ConcurrentHashMap
的底层结构如下:
transient volatile Node<K,V>[] table;
- 底层是一个哈希数组,数组中的每个位置是一个链表或红黑树;
- 每个桶位的节点类型为
Node<K,V>
或 TreeNode<K,V>
(红黑树节点); - 节点结构:
static class Node<K,V> {final int hash;final K key;volatile V val;volatile Node<K,V> next;
}
3. 线程安全机制
核心特性:分段锁 + CAS + synchronized 组合
操作 | 机制说明 |
---|
put(无冲突) | CAS(无锁) |
put(冲突) | synchronized 锁定链表或树头节点 |
get | 无锁(只读 volatile) |
扩容 | synchronized + 多线程协作 |
红黑树转换 | synchronized + TreeBin 封装 |
详细锁机制说明:
1. CAS 操作(乐观锁)
- 用于插入首节点、创建 table 等;
- 通过
Unsafe.compareAndSwapObject
保证原子性; - 高性能,失败时重试。
2. synchronized 锁节点
- 当 CAS 无法完成时(如链表存在),对桶头节点加锁;
- 范围非常小,仅锁当前链表,称为“细粒度锁”。
3. volatile 可见性
- 所有关键字段都用
volatile
修饰,确保多线程间可见性。
4. put 操作流程
步骤简述:
1. 如果 table 未初始化,初始化 table(CAS)
2. 计算 key 的 hash,定位 bucket
3. 如果 bucket 为空,使用 CAS 插入
4. 如果不为空,使用 synchronized 加锁链表或红黑树头节点:- 查找是否存在 key,若存在更新- 若不存在,插入新节点
5. 判断链表长度是否超过阈值(8),转换为红黑树
6. 判断当前元素数量是否超过负载因子,触发扩容
5. 红黑树支持
- 链表长度超过 8,且数组长度 ≥ 64 ⇒ 转为红黑树
- 优点:避免链表退化为 O(n)
- 使用
TreeBin
管理红黑树(不是直接用 TreeMap
)
6. 扩容机制
与 HashMap 不同:
- HashMap:单线程扩容,可能死循环(JDK1.7);
- ConcurrentHashMap:多线程协作扩容,无锁数据迁移。
关键点:
transfer()
方法使用 ForwardingNode
作为占位标记,避免并发冲突;- 线程在不同 bucket 上迁移,不重复工作;
- 保证并发场景下的稳定性和安全性。
7. get 操作流程(无锁)
- 计算 hash 定位 bucket;
- 顺序遍历链表或红黑树;
- 比较 key 值,返回 value;
- 所有节点使用
volatile
,保证可见性。
8. 常用方法说明
方法 | 是否线程安全 | 说明 |
---|
get(K key) | 是 | 无锁读取 |
put(K key, V value) | 是 | CAS + synchronized 组合 |
remove(K key) | 是 | 加锁删除 |
computeIfAbsent() | 是 | 函数式接口并发安全支持 |
forEach() | 是(弱一致性) | 遍历过程支持并发修改 |
9. ConcurrentHashMap vs HashMap vs Hashtable
特性 | HashMap | Hashtable | ConcurrentHashMap |
---|
线程安全 | 否 | 是(全方法加锁) | 是(分段+CAS+synchronized) |
性能 | 高(单线程) | 低(锁粒度粗) | 高(并发环境) |
null 支持 | ✅ 允许 null键和值 | ❌ 不允许 | ❌ 不允许 |
遍历时安全 | ❌ 结构变会抛异常 | ❌ 不 fail-fast | ✅ 弱一致性 |
使用推荐场景 | 单线程场景 | 已淘汰 | 并发读写(推荐) |
10. 注意事项和建议
- 不支持
null
作为 key 或 value; - 遍历期间可以安全地并发修改(弱一致性);
- 不保证元素顺序;
- 适用于高并发缓存、共享数据容器;
- 如果要强一致遍历,建议使用
synchronized
或加快写入频率控制。
11. 总结
优点 | 说明 |
---|
高效线程安全 | 分段锁+CAS+synchronized 保证效率与安全 |
支持高并发 | 多线程读写性能远高于 Hashtable |
局部加锁,粒度细 | 每次只锁单个桶位,不影响整体性能 |
支持红黑树优化 | 高冲突场景避免链表退化,保持高查找性能 |
支持多线程扩容 | 扩容性能高,避免阻塞 |
12. JDK 1.7 vs JDK 1.8:ConcurrentHashMap 实现对比
对比维度 | JDK 1.7 实现(老版) | JDK 1.8 实现(现代) |
---|
底层结构 | Segment 数组 + HashEntry[] | Node[] 数组(无 Segment) |
锁粒度 | Segment 锁(分段锁,16 段默认) | 桶位节点锁(更细,基于 synchronized) |
锁类型 | 显式悲观锁(ReentrantLock) | CAS + synchronized(乐观 + 悲观) |
并发扩容 | 不支持,扩容时阻塞整个表 | 支持多线程协作扩容 |
红黑树优化 | 不支持,链表可能退化为 O(n) | 支持链表转红黑树,保持查询高效 |
put/get 过程 | 需定位 Segment,再定位桶 | 直接定位数组桶,减少层级 |
初始化机制 | 使用 final Segment[] 延迟初始化 | 使用 CAS 创建 table,按需初始化 |
遍历一致性 | 弱一致性 | 弱一致性(更快) |
null 支持 | 不支持 | 不支持 |
性能和扩展性 | 并发度受限于 Segment 数 | 性能更优,适用于更高并发 |
总结:
- JDK 1.7:通过「Segment 分段锁」实现线程安全,锁粒度为段,缺点是扩容时性能差、冲突严重时链表长;
- JDK 1.8:摒弃 Segment,采用「数组 + CAS + synchronized + 红黑树」架构,锁更细、效率更高、并发性能显著提升。