
🍃 予枫 :个人主页
📚 个人专栏 : 《Java 从入门到起飞》《读研码农的干货日常》《Java 面试刷题指南》
💻 Debug 这个世界,Return 更好的自己!
引言
在Java并发编程中,ConcurrentHashMap绝对是"明星组件"------它解决了HashMap线程不安全、Hashtable效率低下的痛点,是多线程环境下操作键值对的首选。但很多开发者只知其然,不知其所以然,尤其JDK7与JDK8中它的底层实现发生了翻天覆地的变化,这也是面试中高频追问的核心考点。本文就带你从底层原理出发,拆解ConcurrentHashMap的并发逻辑,对比JDK7与JDK8的核心差异,帮你吃透面试重点。
文章目录
- 引言
- [一、CONCURRENTHASHMAP 核心定位(为什么需要它?)](#一、CONCURRENTHASHMAP 核心定位(为什么需要它?))
- [二、JDK7 CONCURRENTHASHMAP 底层原理](#二、JDK7 CONCURRENTHASHMAP 底层原理)
-
- [2.1 核心结构(分段锁模型)](#2.1 核心结构(分段锁模型))
- [2.2 核心并发机制(分段锁实现)](#2.2 核心并发机制(分段锁实现))
- [2.3 关键特点](#2.3 关键特点)
- [三、JDK8 CONCURRENTHASHMAP 底层原理](#三、JDK8 CONCURRENTHASHMAP 底层原理)
-
- [3.1 核心结构(数组+链表+红黑树)](#3.1 核心结构(数组+链表+红黑树))
- [3.2 核心并发机制(CAS + synchronized)](#3.2 核心并发机制(CAS + synchronized))
-
- [3.2.1 put操作核心流程](#3.2.1 put操作核心流程)
- [3.2.2 CAS的作用](#3.2.2 CAS的作用)
- [3.2.3 synchronized的优化](#3.2.3 synchronized的优化)
- [3.3 关键特点](#3.3 关键特点)
- [四、JDK7 VS JDK8 核心差异对比](#四、JDK7 VS JDK8 核心差异对比)
- 五、面试官追问环节
- 六、总结
一、CONCURRENTHASHMAP 核心定位(为什么需要它?)
在聊底层原理前,我们先明确一个核心问题:为什么需要ConcurrentHashMap?
我们知道,Java中的Map集合有三个常见实现:
- HashMap:线程不安全,多线程并发修改(如put、remove)可能导致死循环、数据丢失,仅适用于单线程环境;
- Hashtable:线程安全,但采用全局锁(synchronized修饰方法),多线程并发时所有操作都竞争同一把锁,效率极低;
- ConcurrentHashMap:兼顾线程安全和高效并发,通过合理的锁机制,让多线程操作时尽量减少锁竞争,成为并发场景的最优解。
简单来说,ConcurrentHashMap的核心价值就是:在保证线程安全的前提下,最大化提升并发访问效率。
二、JDK7 CONCURRENTHASHMAP 底层原理
JDK7中ConcurrentHashMap的核心设计思路是「分段锁(Segment)」,通过将整个集合分段,实现"锁粒度细化",从而提升并发效率。
2.1 核心结构(分段锁模型)
JDK7的ConcurrentHashMap底层由「Segment数组 + HashEntry数组」组成,结构如下:
ConcurrentHashMap
Segment数组
Segment1
Segment2
SegmentN
HashEntry数组1
HashEntry数组2
HashEntry数组N
HashEntry节点1
HashEntry节点2
HashEntry节点3
- Segment:本质是一个"小的HashMap",自身继承了ReentrantLock,每个Segment对应一把独立的锁;
- HashEntry:存储键值对的节点,与HashMap的Entry结构类似,包含key、value、hash值和下一个节点(链表结构,解决哈希冲突)。
2.2 核心并发机制(分段锁实现)
- 初始化时,ConcurrentHashMap会创建一个Segment数组(默认长度16),每个Segment对应一个HashEntry数组;
- 当执行put操作时,先通过key的hash值计算出对应的Segment索引,找到目标Segment;
- 对该Segment加锁(ReentrantLock),然后在其内部的HashEntry数组中执行put操作(与HashMap的put逻辑类似,哈希冲突时采用链表尾插);
- 操作完成后释放锁,其他线程可以操作其他Segment,互不干扰。
2.3 关键特点
- 锁粒度:以Segment为单位加锁,锁粒度较大(16个Segment对应16把锁);
- 并发效率:多线程操作不同Segment时,无需竞争锁,并发效率比Hashtable高很多;
- 缺点:当多个线程操作同一个Segment时,依然会产生锁竞争,且Segment数组一旦初始化无法扩容,灵活性不足。
三、JDK8 CONCURRENTHASHMAP 底层原理
JDK8对ConcurrentHashMap进行了彻底重构,抛弃了分段锁模型,采用「CAS + synchronized + 红黑树」的组合方案,进一步细化锁粒度,提升并发效率,同时解决了JDK7的弊端。
3.1 核心结构(数组+链表+红黑树)
JDK8的ConcurrentHashMap底层结构与JDK8的HashMap类似,由「Node数组 + 链表 + 红黑树」组成,结构如下:
ConcurrentHashMap
Node数组
Node1
Node2
Node3
链表节点1
链表节点2
红黑树节点1
红黑树节点2
红黑树节点3
- Node数组:存储键值对节点,默认初始容量16,可动态扩容(扩容为原来的2倍);
- Node节点:基础存储单元,包含key、value、hash值和next指针,其中value和next采用volatile修饰,保证可见性;
- 红黑树:当某个Node数组位置的链表长度超过8(默认阈值),且数组长度大于64时,链表会转为红黑树,提升查询效率(从O(n)优化到O(logn))。
3.2 核心并发机制(CAS + synchronized)
JDK8的并发安全实现,核心是"锁粒度细化到节点",结合CAS无锁操作和synchronized锁,具体逻辑如下:
3.2.1 put操作核心流程
- 计算key的hash值,定位到Node数组的对应索引;
- 如果该索引位置为空(未初始化),通过CAS操作创建一个新的Node节点,无需加锁;
- 如果该索引位置不为空,判断该节点是否为"正在扩容的节点"(ForwardingNode),如果是,协助扩容;
- 如果该节点正常,对该节点加synchronized锁(只锁定当前节点,而非整个数组或分段),然后执行put操作:
- 若当前是链表节点,遍历链表,存在则更新value,不存在则尾插;
- 若当前是红黑树节点,按照红黑树规则插入或更新节点;
- 操作完成后,判断链表长度是否超过阈值,若超过则转为红黑树。
3.2.2 CAS的作用
CAS(Compare And Swap)是一种无锁操作,用于解决并发环境下的原子性问题。在ConcurrentHashMap中,CAS主要用于:
- 初始化Node数组的空节点;
- 更新节点的value值;
- 标记节点的状态(如标记为删除、扩容中)。
3.2.3 synchronized的优化
JDK8中synchronized被优化(偏向锁、轻量级锁),性能大幅提升,结合"只锁定单个节点"的设计,使得多线程操作同一数组位置的不同节点时,无需竞争锁,并发效率大幅提升。
3.3 关键特点
- 锁粒度:细化到单个Node节点,锁粒度极小;
- 并发效率:无锁CAS操作 + 优化后的synchronized,并发性能远超JDK7;
- 结构优化:链表转红黑树,提升查询效率;
- 扩容机制:支持动态扩容,扩容时多线程可协助扩容,提升扩容效率。
四、JDK7 VS JDK8 核心差异对比
为了更清晰地掌握两者的区别,我们用表格总结核心差异:
| 对比维度 | JDK7 ConcurrentHashMap | JDK8 ConcurrentHashMap |
|---|---|---|
| 底层结构 | Segment数组 + HashEntry数组(链表) | Node数组 + 链表 + 红黑树 |
| 锁机制 | 分段锁(ReentrantLock),锁粒度为Segment | CAS + synchronized,锁粒度为Node节点 |
| 并发效率 | 多线程操作不同Segment无竞争,同Segment有竞争 | 无锁CAS操作,锁粒度极小,并发效率更高 |
| 扩容机制 | Segment数组不可扩容,仅HashEntry数组可扩容 | 整个Node数组可动态扩容,多线程协助扩容 |
| 哈希冲突解决 | 仅链表尾插 | 链表尾插,长度超阈值转红黑树 |
| 锁类型 | 独占锁(ReentrantLock) | 无锁(CAS)+ 独占锁(synchronized) |
五、面试官追问环节
这部分是面试高频考点,结合前面的原理,帮你提前准备面试官的追问,比纯八股文更实用!
追问1:JDK7的分段锁为什么被JDK8抛弃?
核心原因有3点:
- 锁粒度不够细:虽然分段锁比全局锁好,但16个Segment的锁粒度依然较大,多线程操作同一Segment时仍有锁竞争;
- 扩容灵活性差:Segment数组一旦初始化无法扩容,只能扩容内部的HashEntry数组,限制了并发性能的提升;
- 性能不如优化后的synchronized:JDK8中synchronized经过偏向锁、轻量级锁优化后,性能接近ReentrantLock,且结合CAS无锁操作,比分段锁更高效。
追问2:JDK8中为什么用synchronized而不用ReentrantLock?
主要有2个原因:
- 性能优化后的synchronized足够高效,与ReentrantLock性能差距不大;
- synchronized更贴合JDK8的设计思路:锁粒度细化到Node节点,synchronized的使用更简洁,且无需手动释放锁(自动释放),减少了死锁的风险;
- 结合CAS操作,synchronized负责锁定节点,CAS负责无锁更新,两者配合更高效。
追问3:JDK8中ConcurrentHashMap的扩容机制是怎样的?为什么能支持多线程协助扩容?
- 扩容触发条件:当Node数组的负载因子(默认0.75)超过阈值,或单个链表长度超过8且数组长度≤64时,触发扩容;
- 扩容核心逻辑:创建一个容量为原来2倍的新Node数组,将原数组的节点迁移到新数组中;
- 多线程协助扩容:扩容时,会将原数组分段,每个线程负责迁移一段数据,通过CAS标记迁移状态,避免重复迁移,提升扩容效率。
追问4:ConcurrentHashMap能保证100%线程安全吗?有没有例外情况?
不能保证100%线程安全,有2个常见例外:
- 复合操作不保证原子性:比如getAndPut(先获取再put)、size(计算总元素数)等复合操作,ConcurrentHashMap没有提供原子性支持,需要手动加锁;
- value的修改不保证线程安全:如果value是一个可变对象(如ArrayList),多线程修改value内部的值,ConcurrentHashMap无法保证安全,需要对value本身加锁。
六、总结
ConcurrentHashMap的进化,本质是「锁粒度不断细化、并发机制不断优化」的过程------JDK7用分段锁解决了Hashtable的效率问题,JDK8用CAS+Synchronized+红黑树,进一步突破了分段锁的局限,实现了更高的并发效率。
掌握两者的核心差异和底层原理,不仅能在开发中合理使用ConcurrentHashMap,更能轻松应对面试中的高频追问。记住:面试中考察ConcurrentHashMap,核心就是考察「并发机制」和「JDK7与JDK8的差异」,吃透本文,就能轻松拿捏。