ConcurrentHashMap

本文详细探讨了ConcurrentHashMap的数据结构(数组+链表+红黑树)、如何保证写操作的线程安全、使用LongAdder的计数器实现以及其扩容机制,包括触发时机、步骤和读取数据的高效处理策略。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

ConcurrentHashMap

  1. 存储结构

  2.写操作线程安全

  3.计数器

  4.扩容

  5.获取数据

1、存储结构
HashMap和ConcurrenHashMap在存储结构上是一样的:数组 + 链表 + 红黑树
红黑树出现的原因

因为红黑树需要进行左旋、右旋、变色这些操作来保持平衡,而单链表不需要。

当元素小于 8 个的时候做查询操作时,链表结构能保证查询性能。

当元素大于 8 个的时候, 红黑树搜索时间复杂度是O(logn),而链表是 O(n),此时需要红黑树来加快查询速度,但是新增节点的效率变慢了。

所以啊,如果一开始就用红黑树结构,元素太少,这时新增效率也比较慢,会浪费性能。

为什么链表长度为8才转红黑树:
这个与hashcode碰撞次数的泊松分布有关系,是为了寻找一种时间和空间的平衡。
在负载因子是0.75(HashMap默认)的情况下,单个hash槽内元素个数为8的概率小于百万分之一,将7作为一个中间数,等于7时不做转换,大于等于8才转红黑树,小于等于6转链表。
链表中元素个数为8时的概率已经非常小,再多的就更少了,所以原作者在选择链表元素个数时选择了8,是根据概率统计而选择的。
红黑树结构情况下,如果删除元素,导致红黑树元素个数小于等于6,又会退化为链表。

默认加载因子为什么是0.75:

从时间和空间的角度综合得出的:

1.如果是1.0 当数组的值全部填充了才会发生扩容,此时Hash冲突是避免不了的。链表的操作或者红黑树的操作会牺牲时间来保证空间的利用率

2.如果是0.5 当数组中一半的数据利用了之后就会开始扩容。这时填充的数据少。hash冲突也会减少,底层的链表和红黑树的高度也会降低。查询效率增加。但是这时还有太多的空间没有利用。空间资源浪费了。

所以0.75是综合1和2的逻辑考虑得出。

2、写操作线程安全的保证方式
2.1 往数组上存数据,基于CAS保证安全
2.2 往链表/红黑树存数据,synchronized锁数组元素保证线程安全
2.3JDK1.7中的ConcurrentHashMap是基于分段锁来保证的线程安全
3、计数器的实现
用LongAdder来保证线程安全
LongAdder底层就是基于CAS的方式,再进行+1、-1操作,当然能保证线程安全
AtmoicLong也能保证线程安全,为什么不用AtmoicLong呢?
比如ConcurrentHashMap中记录元素个数的是baseCount,如果有大量线程都想修改baseCount,基于CAS的方式,每次并发只会有一个线程成功,其他失败的线程需要再次获取baseCount的值,再执行CAS..如此反复。
AtmoicLong用的这种方式,其实这样是空转,会导致性能变慢。
这样的CAS操作,会浪费CPU的资源,降低性能。
LongAdder解决上述问题的方式就是,不让每个线程都对baseCount做CAS操作,LongAdder中
提供了很多的CounterCell对象,每个CounterCell内部都有一个long类型的value,线程在做计数
时,可以随机选择一个CounterCell对象对内部的value做+1操作,CounterCell数组的长度最长和你的CPU内核数一致。
CAS是CPU密集操作,能与CPU内核数 ± 1 匹配
baseCount + 所有CounterCell对象的value,最终结果等于ConcurrentHashMap中的元素个数。
4、扩容大致流程:允许多个线程来并发扩容
4.1、扩容触发时机
链表到8。数组长度小于64,扩容数组。
0.75的负载因子,元素个数到了,就得扩。
执行putAll时,如果putAll中的map元素个数当前map无法放下,那就优先扩容。(跟0.75有关
系)将map.size做好运算,与当前的扩容阈值做比较,如果小于扩容阈值,直接添加,大于扩
容阈值,那就优先扩容。
4.2、计算扩容标识戳
标识戳后面会作为标记,代表当前ConcurrentHashMap内部正在扩容数组。
标识戳会记录当前是从多少长度的数组开始做扩容的,避免协助扩容时,出现错误。
4.3、计算每次迁移数据的步长,基于数组长度和CPU内核数计算,最小是16
每个线程会先领取一定长度的迁移数据的任务,领取完,一个位置一个位置的迁移。每次领取任
务的长度是多少,就基于步长来做的。
4.4、创建新数组,长度是老数组的二倍。
4.5、领取迁移数据的索引位置的任务,基于步长得出从哪个索引迁移到哪个索引。
4.6、开始将老数组数据迁移到新数组,等老数组的某个索引位置迁移完之后,会留下一个标记,标
记代表当前位置数据全部迁移到了新数组。
4.7、等老数组的所有数据,都迁移到新数组上之后,最后一个完成迁移数据的线程,会整体再检查
一遍老数组中有没有遗留的数据在。(基本没有)4.8、最后检查完毕之后,迁移结束。
5、获取数据
ConcurrentHashMap在维护红黑树的同时,还会保留一个双向链表的数据结构,读操作,是不阻塞的:
1、若数据是在数组上,查询到就直接返回
2、若数据是在链表上,找到数组的索引位置后,next....next一个一个往下找,找到就返回
3、若数据在红黑树上
   3.1 如果有写线程在红黑树上进写数据操作,那么读线程去读取一个双向链表查询数据
   3.2 如果没有写线程在操作红黑树,那就在红黑树上正常的left和right左旋和右旋的去找对应数据
4、如果定位的索引位置是一个标记(标记为正在扩容)
     直接基于标记定位到新数组的位置,去新数组找数据。
### ConcurrentHashMap 使用指南 ConcurrentHashMapJava 提供的一种线程安全的 Map 实现,专为高并发环境设计。相比传统的 HashMap 和 Hashtable,它在多线程访问时表现出更高的性能和安全性。通过分段锁机制与 CAS(Compare and Swap)操作,ConcurrentHashMap 能够支持多个线程同时读写而无需全局加锁。 #### 线程安全特性 ConcurrentHashMap 的线程安全特性来源于其内部实现机制。不同于 Hashtable 使用的 synchronized 方法进行全局加锁,ConcurrentHashMap 在 JDK 1.7 及之前版本中采用 **分段锁(Segment Locking)** 技术[^3]。每个 Segment 相当于一个独立的 HashTable,允许多个线程同时访问不同的 Segment,从而减少锁竞争,提高并发性能。 在 JDK 1.8 中,ConcurrentHashMap 进一步优化了其实现,使用 **桶锁(Bucket-level Locking)** 和 **红黑树结构** 来提升性能。此时,锁的粒度更细,仅对当前操作的桶进行加锁,进一步提高了并发能力[^5]。 #### 常用方法与并发操作 ConcurrentHashMap 提供了一系列线程安全的操作方法,例如: - `putIfAbsent(K key, V value)`:只有当指定键不存在或映射值为 null 时才插入键值对。 - `computeIfPresent(K key, BiFunction<? super K, ? super V, ? extends V> remappingFunction)`:如果键存在,则根据提供的函数重新计算该键的值。 - `compute(K key, BiFunction<? super K, ? super V, ? extends V> remappingFunction)`:无论键是否存在,都尝试重新计算其映射值。 - `forEach(BiConsumer<? super K, ? super V> action)`:对每个键值对执行指定操作。 这些方法可以在不使用外部同步的情况下安全地用于多线程环境中[^2]。 #### 示例代码 以下是一个简单的示例,展示如何在 Spring Boot 应用中使用 ConcurrentHashMap 管理共享数据: ```java import java.util.concurrent.ConcurrentHashMap; public class UserService { private final ConcurrentHashMap<String, Integer> userCountMap = new ConcurrentHashMap<>(); public void incrementUserCount(String department) { userCountMap.compute(department, (key, oldValue) -> (oldValue == null) ? 1 : oldValue + 1); } public int getUserCount(String department) { return userCountMap.getOrDefault(department, 0); } } ``` 上述代码中的 `compute` 方法确保在多线程环境下对用户计数的更新是线程安全的,无需额外的同步控制[^4]。 #### 适用场景 ConcurrentHashMap 非常适合以下场景: - **缓存系统**:如 Web 应用中缓存用户信息、会话状态等。 - **计数器**:统计在线人数、请求次数等。 - **任务调度**:管理待处理任务队列或资源池。 - **配置中心**:存储可动态更新的配置项。 由于其良好的并发性能,ConcurrentHashMap 成为了构建高性能服务端应用的重要组件之一。 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值