ConcurrentHashMap 原理详解
ConcurrentHashMap 原理详解
一、概述
ConcurrentHashMap 是 Java 并发包(java.util.concurrent)中提供的线程安全的哈希表实现,专为高并发场景设计。它解决了 HashMap 在并发环境下的线程安全问题,同时相比 Hashtable 提供了更高的并发性能。
ConcurrentHashMap 的核心设计目标是在保证线程安全的前提下,尽可能提高并发访问效率,主要通过分段锁(JDK7)或CAS+synchronized(JDK8)机制实现。
二、JDK7 中的 ConcurrentHashMap 原理
2.1 数据结构
JDK7 的 ConcurrentHashMap 采用 "分段数组 + HashEntry 链表" 的结构:
- Segment 数组:本质是一个可重入锁(ReentrantLock),每个 Segment 包含一个 HashEntry 数组
- HashEntry 数组:存储实际的键值对,每个 HashEntry 是一个链表节点
- 分段锁机制:每个 Segment 独立加锁,不同 Segment 上的操作可以并发执行
结构示意图:
|
ConcurrentHashMap ┌─────────────────────────────────────────────────────┐ │ segments: Segment[] │ │ ┌─────────┐ ┌─────────┐ ┌─────────┐ ... │ │ │Segment 0│ │Segment 1│ │Segment 2│ │ │ │┌───────┐│ │┌───────┐│ │┌───────┐│ │ │ ││Entry[]││ ││Entry[]││ ││Entry[]││ │ │ │└───────┘│ │└───────┘│ │└───────┘│ │ │ └─────────┘ └─────────┘ └─────────┘ │ └─────────────────────────────────────────────────────┘ |
2.2 核心原理
- 分段锁机制
- 整个哈希表被分为多个 Segment(默认 16 个)
- 操作某段数据时,仅对该段加锁,其他段可并发访问
- 理论上,并发度为 Segment 的数量(默认 16)
- 定位元素流程
- 计算 key 的哈希值
- 确定该 key 属于哪个 Segment(hash >>> segmentShift & segmentMask)
- 在对应 Segment 中计算 HashEntry 数组的索引
- 操作对应位置的链表
- get 操作
- 无需加锁,通过 volatile 关键字保证可见性
- 遍历链表查找元素,若期间发生修改,volatile 保证能看到最新值
- put 操作
- 计算 Segment 位置并获取该 Segment 的锁
- 在 Segment 内部执行类似 HashMap 的 put 操作
- 操作完成后释放锁
- 若 Segment 中元素数量超过阈值,仅对该 Segment 进行扩容
- size 操作
- 先尝试无锁方式计算总大小(最多重试 3 次)
- 若期间发生修改,对所有 Segment 加锁后再计算
三、JDK8 中的 ConcurrentHashMap 原理
3.1 数据结构
JDK8 对 ConcurrentHashMap 进行了重大重构,采用与 HashMap 类似的 "数组 + 链表 + 红黑树" 结构,但通过更细粒度的同步机制实现线程安全:
- 取消了 Segment 分段锁,直接使用 Node 数组存储数据
- 引入红黑树优化长链表(阈值 8)
- 使用 CAS 操作和 synchronized 关键字实现同步
结构示意图:
|
ConcurrentHashMap ┌─────────────────────────────────────────────────────┐ │ table: Node[] │ │ ┌───────┐ ┌───────┐ ┌───────┐ ... │ │ │Node │ │Node │ │TreeNode│ │ │ │(链表) │ │(链表) │ │(红黑树)│ │ │ └───────┘ └───────┘ └───────┘ │ └─────────────────────────────────────────────────────┘ |
3.2 核心原理
- 同步机制
- 采用CAS + synchronized组合实现线程安全
- 对链表头节点或红黑树根节点使用 synchronized 加锁
- 粒度更细,仅锁定冲突的链表或红黑树,而非整个分段
- 关键内部类
- Node:基础节点,存储键值对,value 和 next 使用 volatile 修饰
- TreeNode:红黑树节点,继承自 Node
- ForwardingNode:扩容时使用的节点,标记当前节点已迁移
- get 操作
- 无锁操作,通过 volatile 保证可见性
- 计算索引位置,遍历链表或红黑树查找元素
- 若遇到 ForwardingNode,说明正在扩容,会到新表中查找
- put 操作
|
1. 计算key的哈希值 2. 若数组未初始化,初始化数组 3. 计算索引位置,获取该位置的节点f 4. 若f为null,尝试用CAS插入新节点 5. 若f为ForwardingNode,说明正在扩容,帮助迁移并重新尝试put 6. 否则,对f加synchronized锁: a. 遍历链表查找是否存在相同key b. 存在则替换value,不存在则插入新节点 c. 检查是否需要将链表转为红黑树 7. 若插入了新节点,检查是否需要扩容 |
- 扩容机制
- 支持多线程并发扩容
- 扩容时设置 sizeCtl 为负数值标记扩容状态
- 每个线程负责迁移一部分节点
- 使用 ForwardingNode 标记已迁移的桶
- size 操作
- 维护 baseCount 和 counterCells 两个变量
- 并发更新时通过 CAS 更新 counterCells
- 计算总大小时合并 baseCount 和 counterCells 的值
四、JDK7 与 JDK8 的 ConcurrentHashMap 对比
|
特性 |
JDK7 |
JDK8 |
|
数据结构 |
Segment 数组 + HashEntry 链表 |
Node 数组 + 链表 + 红黑树 |
|
同步机制 |
基于 ReentrantLock 的分段锁 |
CAS + synchronized |
|
并发度 |
由 Segment 数量决定(默认 16) |
理论上与数组长度相当 |
|
锁粒度 |
Segment 级别 |
链表 / 红黑树级别 |
|
扩容 |
单个 Segment 独立扩容 |
全表扩容,支持多线程协作 |
|
红黑树支持 |
不支持 |
支持 |
|
内存占用 |
较高(多一层 Segment) |
较低 |
|
性能 |
中等 |
高(细粒度锁) |
五、ConcurrentHashMap 的优势
- 高并发性能
- 细粒度的同步机制,允许多个线程同时访问不同的桶
- 读操作无锁,性能接近 HashMap
- 线程安全保证
- 所有修改操作都是线程安全的
- 提供了弱一致性的迭代器,不会抛出 ConcurrentModificationException
- 丰富的原子操作
- 提供 putIfAbsent、remove、replace 等原子操作
- 避免了手动加锁实现原子操作的繁琐
- 内存可见性
- 通过 volatile 关键字保证节点的可见性
- 确保一个线程的修改能被其他线程看到
六、适用场景
- 高并发环境下的键值对存储
- 缓存系统实现
- 多线程共享数据的场景
- 需要频繁进行读操作,同时有一定写操作的场景
相比 Hashtable(全表锁)和 Collections.synchronizedMap(全表锁),ConcurrentHashMap 在高并发场景下具有明显的性能优势,是线程安全哈希表的首选。
本文来自博客园,作者:诸葛匹夫,转载请注明原文链接:https://www.cnblogs.com/shenxingzhuge/p/19070089
卡里离冰冷的40个亿还差39多个亿
浙公网安备 33010602011771号