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修饰,保证多线程可见性。1
2
3
4
5
6
7static 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 | // 哈希表数组(volatile 修饰,保证扩容后可见性) |
核心方法解析(JDK 8)
1. put(K key, V value):线程安全插入
put 方法通过 CAS 和 synchronized 实现并发插入,核心逻辑如下:
1 | public V put(K key, V value) { |
并发控制关键点:
- 空桶插入用 CAS 避免加锁,提升效率;
- 非空桶用
synchronized锁定头节点,仅阻塞同桶的并发写入,不同桶可并行; - 扩容时通过
ForwardingNode引导其他线程协助扩容,加速扩容过程。
2. get(Object key):无锁读操作
读操作全程无锁,依赖 volatile 保证节点值的可见性:
1 | public V get(Object key) { |
无锁原理:
- 数组
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(全表) |
| 适用场景 | 高并发读写 | 单线程 / 无并发 | 旧系统兼容 |