在 Java 并发编程中,HashMap 作为最常用的哈希表实现,却存在线程不安全 的致命缺陷 ------ 在多线程环境下扩容可能引发死循环、数据丢失等问题。而 Hashtable 虽然线程安全,却通过粗暴的全表锁(synchronized 修饰方法)导致并发性能极差,无法满足高并发场景需求。
此时,ConcurrentHashMap 应运而生,它完美平衡了线程安全 与高并发性能,成为 Java 并发集合中的标杆实现。本文将基于 JDK 1.8 版本,深度拆解 ConcurrentHashMap 的核心设计思想,带你理解它如何做到 "安全又高效"。
一、前置知识:为什么弃用 Hashtable 和 synchronizedMap?
在理解 ConcurrentHashMap 之前,先明确传统线程安全哈希表的痛点:
- Hashtable :所有方法(put、get、size 等)都加
synchronized锁,相当于整个哈希表同一时间只能被一个线程操作,并发场景下锁竞争激烈,性能瓶颈明显。 - Collections.synchronizedMap :底层也是通过
synchronized锁实现,锁粒度同样是整个 Map 对象,本质和 Hashtable 无区别。
这两种实现都是 **"独占锁" 思想 **,完全牺牲了并发能力,只适合低并发场景。而 ConcurrentHashMap 的核心设计目标,就是缩小锁粒度、减少锁竞争、最大化并发效率。
二、JDK 1.8 ConcurrentHashMap 核心设计思想
ConcurrentHashMap 的进化是 Java 并发优化的经典案例,JDK 1.8 彻底抛弃了 1.7 版本的分段锁(Segment) 设计,采用 数组+链表/红黑树 + CAS + synchronized 锁 的组合方案,实现了更细粒度的并发控制。
1. 数据结构:从分段锁到 "节点级锁"
JDK 1.7 中,ConcurrentHashMap 把哈希表分成 16 个 Segment(分段),每个 Segment 独立加锁,并发度为 16。但这种设计存在结构复杂、查询效率低的问题。
JDK 1.8 直接简化为 HashMap 同款数据结构:
Node[] 数组(哈希桶) + 链表 + 红黑树
- 链表长度超过 8 且数组长度≥64 时,自动转为红黑树,将查询时间复杂度从 O (n) 降至 O (logn);
- 锁粒度从分段 缩小为每个哈希桶的头节点,即:只锁住当前操作的数组节点,不影响其他节点的并发操作。
这是 ConcurrentHashMap 高性能的基础------ 锁的粒度越细,并发冲突概率越低,性能越高。
2. 核心并发控制:CAS + synchronized
JDK 1.8 摒弃了重量级锁,采用无锁(CAS)+ 轻量级锁(synchronized) 组合,这是最核心的设计思想:
- 读操作:完全无锁 所有元素的
val和next都用volatile关键字修饰,保证多线程间的可见性。读数据时无需加锁,直接读取,性能和 HashMap 一致。 - 写操作:先 CAS 后锁
- 向哈希桶插入新节点时,先用 CAS 无锁尝试:如果目标数组节点为空,直接通过 CAS 原子性插入节点,成功则无锁完成;
- CAS 失败(说明有线程竞争):才对当前哈希桶的头节点 加
synchronized锁,执行插入 / 更新操作。
设计精髓:大部分并发场景下,写操作可以通过 CAS 无锁完成;只有真正冲突时,才加极小粒度的锁,最大程度减少锁竞争。
3. 关键优化:防止并发冲突的细节设计
ConcurrentHashMap 还有很多匠心设计,彻底解决了并发安全问题:
(1)volatile 保证可见性
核心数组 transient volatile Node<K,V>[] table、节点的 val 和 next 都用 volatile 修饰:
- 禁止指令重排;
- 保证一个线程修改后,其他线程能立即看到最新值,解决了 "脏读" 问题。
(2)扩容优化:并发扩容,无全表阻塞
传统 Map 扩容时会阻塞所有线程,而 ConcurrentHashMap 支持多线程协同扩容:
- 扩容时,每个线程只负责迁移自己的哈希桶,互不干扰;
- 扩容期间,读操作可以正常执行;写操作会辅助扩容,提升效率;
- 用
forwardingNode标记扩容中的节点,保证线程安全。
(3)不允许 key/value 为 null
HashMap 允许 key 和 value 为 null,但 ConcurrentHashMap 严格禁止:
- 原因:并发场景下,无法区分
get(null)是 "key 不存在" 还是 "value 本身为 null",会导致判断歧义; - 这是为了并发安全做出的取舍。
(4)size 计算:无锁统计
JDK 1.7 用分段计数统计元素数量,性能一般;JDK 1.8 用 baseCount + CAS 累加 实现无锁统计,避免了全局锁,高效获取元素总数。
三、ConcurrentHashMap 核心工作流程(以 put 为例)
通过 put 方法的执行逻辑,能直观理解它的设计思想:
- 校验 key/value 不为 null,否则抛异常;
- 若哈希桶数组未初始化,CAS 无锁初始化;
- 计算 key 的哈希值,定位目标数组节点;
- 若目标节点为空:CAS 原子性插入新节点,无锁完成;
- 若目标节点不为空(哈希冲突):
- 若节点是
forwardingNode(正在扩容):当前线程协助扩容; - 否则:对目标头节点加 synchronized 锁,遍历链表 / 红黑树插入 / 更新节点;
- 若节点是
- 插入后判断是否需要树化(链表长度≥8),是则转为红黑树;
- 最后无锁更新元素数量。
整个流程:无锁优先,锁仅加在冲突的单个节点上,并发效率拉满。
四、ConcurrentHashMap 设计思想总结
ConcurrentHashMap 的核心设计哲学,是 **"分而治之" + 无锁优先 **:
- 细粒度锁:锁从全表 → 分段 → 单个节点,最小化锁范围;
- 无锁优化:读操作完全无锁,写操作优先 CAS 无锁,仅冲突时加锁;
- volatile 保安全:保证多线程间数据可见性,杜绝脏读;
- 并发扩容:多线程协同扩容,避免全表阻塞;
- 结构精简:数组 + 链表 / 红黑树,兼顾查询性能与内存效率。
五、使用场景
ConcurrentHashMap 适用于高并发读写的场景:
- 分布式缓存、本地缓存;
- 多线程共享数据的统计、存储;
- 高并发接口中的数据容器。
只要是多线程环境下需要线程安全的 Map,优先选择 ConcurrentHashMap,而非 Hashtable 或 synchronizedMap。