知其然要知其所以然,探索每一个知识点背后的意义,你知道的越多,你不知道的越多,一起学习,一起进步,如果文章感觉对您有用的话,关注、收藏、点赞,有困惑的地方可以评论,我们一起探讨!
1. 什么是 ConcurrentHashMap?
ConcurrentHashMap 是 Java 并发编程中用于高并发场景的线程安全哈希表实现,属于 java.util.concurrent 包。它通过优化锁粒度和数据结构设计,提供了高效的并发读写性能,解决了传统 Hashtable 和 Collections.synchronizedMap 的全局锁性能瓶颈问题。
2. 核心作用
- 线程安全:支持多线程环境下的安全读写操作。
- 高并发性能:通过分段锁(Java 7)或 CAS + 细粒度锁(Java 8)减少锁竞争。
- 动态扩展:自动扩容哈希表,适应数据量变化。
- 高效查询:Java 8 引入红黑树优化链表查询效率。
3. Java 7 和 Java 8 的对比分析
3.1 底层数据结构
Java 7:分段锁(Segment)
Java 8:Node 数组 + 红黑树
3.2 线程安全实现方式
Java 7:分段锁
- 实现方式:
- 每个
Segment 继承自 ReentrantLock(当时synchronized还没有被优化),写操作时锁住对应段。
- 读操作无锁(通过
volatile 保证可见性)。
- 锁粒度:段级别(默认 16 段),不同段的操作可并发。
- 示例流程(
put 操作):
sequenceDiagram
participant Client
participant ConcurrentHashMap
participant Segment
participant HashEntry
Client->>ConcurrentHashMap: put(key, value)
ConcurrentHashMap->>ConcurrentHashMap: 计算哈希值
ConcurrentHashMap->>Segment: 定位到段
Segment->>Segment: lock()
Segment->>HashEntry: 插入/更新链表
Segment->>Segment: unlock()
ConcurrentHashMap-->>Client: 完成
Java 8:CAS + 细粒度锁
- 实现方式:
- CAS 操作:无锁化插入头节点(桶为空时)。
- synchronized 锁:桶不为空时,锁住链表头节点(JDK1.6 以后 synchronized 锁做了很多优化)。
- 红黑树优化:减少长链表查询时间。
- 锁粒度:桶级别(单个
Node)。
- 示例流程(
put 操作):
sequenceDiagram
participant Client
participant ConcurrentHashMap
participant Node
participant TreeBin
Client->>ConcurrentHashMap: put(key, value)
ConcurrentHashMap->>ConcurrentHashMap: 计算哈希值
ConcurrentHashMap->>Node: 定位到桶
alt 桶为空
ConcurrentHashMap->>Node: CAS 插入新节点
else 桶非空
ConcurrentHashMap->>Node: synchronized 锁住头节点
Node->>Node: 遍历链表/树
alt 链表长度 < 8
Node->>Node: 插入链表
else 链表长度 ≥ 8
Node->>TreeBin: 转换为红黑树
TreeBin->>TreeBin: 插入树节点
end
ConcurrentHashMap->>Node: 释放锁
end
ConcurrentHashMap-->>Client: 完成
3.3 加锁方式对比
| 特性 | Java 7 | Java 8 |
|---|
| 锁类型 | ReentrantLock(显式锁) | synchronized(隐式锁) |
| 锁粒度 | 段级别(默认 16 段) | 桶级别(单个 Node) |
| 读操作 | 无锁(通过 volatile) | 无锁(通过 volatile + CAS) |
| 写操作 | 锁住段 | 锁住桶头节点或使用 CAS |
| 数据结构优化 | 链表 | 链表 + 红黑树 |
4. 设计动机与优势
Java 7 的设计
- 动机:
- 解决
Hashtable 全局锁的性能问题。
- 通过分段锁减少锁竞争。
- 优势:
- 缺点:
- 段数量固定,无法动态调整。
- 内存开销较大(每个段独立维护结构)。
Java 8 的设计
- 动机:
- 进一步减少锁竞争,提升高并发性能。
- 解决长链表查询效率低的问题。
- 优势:
- 更细粒度的锁:锁住单个桶,并发度更高。
- 红黑树优化:链表查询复杂度从 O(n) 降至 O(log n)。
- 动态扩容:更灵活的内存管理。
- 缺点:
5. 关键改进总结
| 改进点 | Java 7 | Java 8 |
|---|
| 锁粒度 | 段级别(粗粒度) | 桶级别(细粒度) |
| 数据结构 | 链表 | 链表 + 红黑树 |
| 内存开销 | 较高(每个段独立结构) | 较低(统一 Node 结构) |
| 查询性能 | O(n) | O(log n)(红黑树优化) |
| 并发度 | 依赖段数量(默认 16) | 更高(桶数量动态扩展) |
6. 适用场景
- Java 7:
- 中等并发场景,对内存开销不敏感。
- 不需要处理极端长链表的情况。
- Java 8:
- 高并发场景,需支持动态扩展。
- 需优化长链表的查询性能。
7. 思考问题
- ConcurrentHashMap的key和value为什么不可以为null?:
- 主要是为了让数据处理更清晰明确,避免这种难以区分的状况, 若 key 为 null ,就难以判断是这个键原本就不存在于 ConcurrentHashMap 中,还是仅仅是被设置为 null 。同理,若 value 为 null ,也无法明确是这个值真实存在于其中,还是因为找不到对应键而返回的 null
- ConcurrentHashMap能保证复合操作的原子性吗?:
- 不能
- 定义:复合操作是指由多个基本操作(如put、get、remove、containsKey等)组成的操作,例如先判断某个键是否存在containsKey(key),然后根据结果进行插入或更新put(key, value)。这种操作在执行过程中可能会被其他线程打断,导致结果不符合预期。
- 那如何保证 ConcurrentHashMap 复合操作的原子性呢?:
- ConcurrentHashMap 提供了一些原子性的复合操作(底层其实是CAS操作),如 putIfAbsent、compute、computeIfAbsent 、computeIfPresent、merge等。这些方法都可以接受一个函数作为参数,根据给定的 key 和 value 来计算一个新的 value,并且将其更新到 map 中。putIfAbsent:如果键之前不存在于映射中,返回 null,并将 key 和 value 插入到映射中。如果键已经存在,返回对应的值,不会更新映射中的值
- 其实调用方法时加锁也是可以的,但是不建议,因为这样违背了使用 ConcurrentHashMap 的初衷。在使用 ConcurrentHashMap 的时候,尽量使用这些原子性的复合操作方法来保证原子性
总结
- Java 7:通过分段锁实现并发控制,简单但扩展性有限。
- Java 8:通过 CAS + 细粒度锁 + 红黑树,显著提升高并发性能和查询效率。
- 设计演进的核心思想(都是分治思想):
- 减少锁竞争:从段锁到桶锁,锁粒度更细。
- 优化数据结构:引入红黑树解决链表性能问题。
- 无锁化操作:利用 CAS 减少线程阻塞。