| title | date | tags | categories |
|---|---|---|---|
ConcurrentHashMap |
2025-01-07 01:56:23 -0800 |
💠
-
- 1.1. 结构
- 1.2. 构造函数
- 1.3. put
- 1.4. get
- 1.5. remove
- 1.6. Tips
- 1.7. ConcurrentHashMap 与 HashMap
- 1.8. 线程安全机制(JDK 1.8)
- 1.8.1. CAS操作
- 1.8.2. synchronized锁
- 1.8.3. volatile关键字
- 1.8.4. 扩容机制
- 1.9. 总结
💠 2025-12-16 20:28:35
参考: ConcurrentHashMap源码分析(JDK8)
参考: Java并发编程:ConcurrentHashMap
注意: JDK1.8 之前使用分段锁(Segment),JDK1.8 之后使用 CAS + synchronized 实现线程安全
ConcurrentHashMap的数据结构与HashMap类似,都是数组 + 链表 + 红黑树的结构,但实现了线程安全。
- 修改HashMap,并不需要将整个结构都锁住,只要锁住即将修改的桶(就是单个元素)
- 好的HashMap 实现,在读取时不需要锁,写入时只要锁住要修改的单个桶 Java能达到这个标准,但是需要程序员去操作底层的细节才能实现
ConcurrentHashMap类 还实现了ConcurrentMap接口,有些提供了还提供了原子操作的新方法putIfAbsent()如果还没有对应键,就把键/值添加进去remove()如果键存在而且值与当前状态相等,则用原子方式移除键值对replace()API 为HashMap中原子替换的操作方法提供了两种不同的形式
- key value 均不允许为null
1.7 到 1.8 改动
- JDK 1.7: 采用分段锁(Segment)机制,将数据分成多个段,每个段独立加锁
- JDK 1.8: 采用 CAS + synchronized 机制,锁的粒度更细,只锁住数组中的单个桶(Node)
- 数据结构:取消了 Segment 分段锁的数据结构,取而代之的是数组+链表+红黑树的结构。
- 保证线程安全机制:JDK1.7 采用Segment 的分段锁机制实现线程安全,其中 Segment 继承自 ReentrantLock 。JDK1.8采用CAS+synchronized保证线程安全。
- 锁的粒度:JDK1.7 是对需要进行数据操作的 Segment 加锁,JDK1.8调整为对每个数组元素加锁(Node)。
- 链表转化为红黑树:定位节点的 hash 算法简化会带来弊端,hash冲突加剧,因此在链表节点数量大于 8(且数据总量大于等于 64)时,会将链表转化为红黑树进行存储。
- 查询时间复杂度:从JDK1.7的遍历链表O(n), JDK1.8 变成遍历红黑树O(logN)。
ConcurrentHashMap的迭代器是弱一致性(weakly-consistent)的,和 ConcurrentSkipListMap 一样,它不会抛出ConcurrentModificationException异常,但是无法保证迭代器遍历的元素是一个完整的快照。因此,在迭代器遍历时,可能会遗漏或重复遍历某些元素。
源码层面怎么做到的 不复制整个数组(不像 CopyOnWrite);
不维护 modCount(所以检测不到并发修改);
分段推进(Java 8+ 是数组+链表/树,迭代器按 slot 顺序读);
遇到被并发搬走的桶 → 直接跳过或读到新值,不会重试。
默认初始容量 16 和 默认负载因子 0.75,与HashMap相同。
- 如果手动指定了初始容量,会根据
tableSizeFor方法计算得到一个大于初始容量的最小的2的指数值 - 支持并发级别(concurrencyLevel)参数(JDK 1.7),JDK 1.8 中该参数仅用于兼容性,实际不再使用
线程安全的put操作实现
- 计算key的hash值(与HashMap相同:
(h = key.hashCode()) ^ (h >>> 16)); if桶(数组)大小为0,则初始化桶(使用CAS保证线程安全);if桶中key的下标上没有节点对象,则使用CAS直接插入;else if该位置正在扩容(hash值为MOVED),则帮助扩容;else如果有节点对象synchronized锁住该桶的头节点;ifkey的下标上有节点对象且key相同,则替换旧值;else if有节点对象且为树节点,则调用树节点的putTreeVal()插入key对应的节点对象;else则遍历桶对应的链表查找key是否存在于链表中;if找到了对应key的节点,则替换旧值;if没找到对应key的节点,则尾插法插入节点并判断是否需要树化;
- 每当插入新节点,则使用CAS更新size并判断是否需要扩容;
关键点:
- 使用CAS进行无锁操作(初始化、插入空桶)
- 使用synchronized锁住单个桶,而不是整个Map
- 支持多线程协助扩容(helpTransfer)
线程安全的get操作实现
- 计算key的hash值;
- 定位到对应的桶;
if桶的头节点就是要找的key,直接返回value;else if头节点的hash值小于0(表示正在扩容或为树节点),调用对应的find方法查找;else遍历链表查找对应的key;
关键点:
- get操作是无锁的,通过volatile保证可见性
- 支持在扩容过程中进行查找(ForwardingNode)
线程安全的remove操作实现
- 计算key的hash值;
- 定位到对应的桶;
synchronized锁住该桶的头节点;- 查找对应的节点并删除;
- 如果删除后链表长度小于6,进行反树化;
- size/isEmpty 等是弱一致,不能保证瞬时精确
- HashMap: 线程不安全,多线程环境下可能出现数据丢失、死循环等问题
- ConcurrentHashMap: 线程安全,支持高并发读写操作
- HashMap: 无锁,性能高但不安全
- ConcurrentHashMap (JDK 1.7): 分段锁(Segment),锁粒度较粗
- ConcurrentHashMap (JDK 1.8): CAS + synchronized,锁粒度更细,性能更好
- 单线程: HashMap性能略优于ConcurrentHashMap
- 多线程: ConcurrentHashMap性能远优于HashMap + synchronized
- ConcurrentHashMap的key和value都不能为null(HashMap允许null)
- ConcurrentHashMap的size()方法返回的是近似值(多线程环境下)
- ConcurrentHashMap支持弱一致性迭代器
- 用于无锁的初始化、插入空桶等操作
- 使用
sun.misc.Unsafe类提供的CAS方法
- 只锁住单个桶(Node),而不是整个Map
- 锁的粒度更细,减少锁竞争
- Node的val和next字段使用volatile修饰,保证可见性
- 数组table使用volatile修饰,保证扩容时的可见性
- 支持多线程协助扩容(helpTransfer)
- 使用ForwardingNode标记正在扩容的桶
- 扩容时不影响读操作
