java8中为什么放弃了ConcurrentMap的分段锁,采取了什么新的策略

Java8中ConcurrentHashMap放弃分段锁策略,采用synchronized+CAS实现线程安全。此举旨在减少内存开销,提高GC效率,利用JVM优化如锁粗化、锁消除等。新方案针对链表首节点使用synchronized,确保线程安全同时,通过CAS尝试插入元素,提高并发性能。

java8中为什么放弃了ConcurrentMap的分段锁,采取了什么新的策略

前言

ConcurrentMap,它是一个接口,是一个能够支持并发访问的java.util.map集合;

ConcurrentHashMap是一个线程安全,并且是一个高效的HashMap。

spring 缓存注解 通过查看源代码发现将数据存在ConcurrentMap中

它的原理是引用了内部的 Segment ( ReentrantLock ) 分段锁,和hashmap一样,在jdk1.7中ConcurrentHashMap的底层数据结构是数组加链表。和hashmap不同的是ConcurrentHashMap中存放的数据是一段段的,即由多个Segment(段)组成的。每个Segment中都有着类似于数组加链表的结构。

保证在操作不同段 map 的时候, 可以并发执行, 操作同段 map 的时候,进行锁的竞争和等待。从而达到线程安全, 且效率大于 synchronized。

但是在 Java 8 之后, JDK 却弃用了这个策略,重新使用了 synchronized+cas。

弃用原因

通过 JDK 的源码和官方文档看来, 他们认为的弃用分段锁的原因由以下几点:

  1. 加入多个分段锁浪费内存空间。
  2. 生产环境中, map 在放入时竞争同一个锁的概率非常小,分段锁反而会造成更新等操作的长时间等待。
  3. 为了提高 GC 的效率

什么是可重入锁(递归锁)

定义:当某个线程已经获得某个锁,可以再次获取锁而不会发生死锁,可以多次获取相同的锁。
优点:可以再一定程度上避免死锁。

新的方案

既然弃用了分段锁, 那么一定由新的线程安全方案, 我们来看看源码是怎么解决线程安全的呢?(源码保留了segment 代码, 但并没有使用)

put
首先通过 hash 找到对应链表过后, 查看是否是第一个object, 如果是, 直接用cas原则插入,无需加锁。
然后, 如果不是链表第一个object, 则直接用链表第一个object加锁,这里加的锁是synchronized,虽然效率不如 ReentrantLock, 但节约了空间,这里会一直用第一个object为锁, 直到重新计算map大小, 比如扩容或者操作了第一个object为止。

为什么是synchronized,而不是ReentranLock

  1. 减少内存开销
    假设使用可重入锁来获得同步支持,那么每个节点都需要通过继承AQS来获得同步支持。但并不是每个节点都需要获得同步支持的,只有链表的头节点(红黑树的根节点)需要同步,这无疑带来了巨大内存浪费。
  2. 获得JVM的支持
    可重入锁毕竟是API这个级别的,后续的性能优化空间很小。
    synchronized则是JVM直接支持的,JVM能够在运行时作出相应的优化措施:锁粗化、锁消除、锁自旋等等。这就使得synchronized能够随着JDK版本的升级而不改动代码的前提下获得性能上的提升。
### Java 1.8 中 ConcurrentHashMap 与 HashMap 的底层数据结构设计差异 #### 数据结构的设计目标 在 JDK 1.8 中,`ConcurrentHashMap` 和 `HashMap` 均采用了基于数组加链表(或红黑树)的混合数据结构来存储键值对。然而,两者的核心设计理念存在显著不同:`HashMap` 主要追求高效的单线程操作性能,而 `ConcurrentHashMap` 则专注于提供高并发场景下的线程安全性。 --- #### 数组长度的选择 对于 `HashMap`,其底层数组长度始终为 2 的幂次方,这一特性通过 `(hash & (length - 1))` 计算索引来实现高效定位[^4]。这种设计能够减少因扩容而导致的位置调整开销,从而提升整体性能。 相比之下,`ConcurrentHashMap` 同样采用 2 的幂作为数组大小,但在内部实现了更复杂的分段锁机制以支持多线程访问。它将整个数据结构划分为多个独立的部分(Segment),每个 Segment 类似于一个小规模的 `HashMap`,并通过不同的锁控制各个部分的操作[^2]。 --- #### 锁机制的区别 - **HashMap**: 不具备任何内置的同步机制,因此在多线程环境中可能会引发诸如死循环等问题。即使是在 JDK 1.8 中对其尾插法进行了优化以避免某些特定问题,但它本质上仍不适合直接应用于并发场景[^3]。 - **ConcurrentHashMap**: 使用了一种称为“**CAS + synchronized**”的技术组合来进行无锁化更新尝试以及必要的局部锁定。更重要的是,在 JDK 1.8 中重构了原有的 Segments 结构,改为更加细粒度化的节点级锁管理方案——即当某个桶中的元素数量超过一定阈值时才会对该区域施加写保护;而对于读取操作,则几乎完全无需阻塞即可完成[^2]。 --- #### 处理哈希冲突的方式 无论是哪种集合类,面对不可避免的哈希碰撞都需要采取适当措施加以应对: - 在 `HashMap` 中,如果链表长度达到指定界限(默认为 8),则会自动转换成一棵平衡二叉搜索树(即红黑树)。这一步骤有助于降低极端条件下可能出现的时间复杂度恶化风险至接近 \(O(\log n)\)。 - 对应到 `ConcurrentHashMap` 上面来说,除了同样拥有上述功能之外,还额外增强了针对共享资源的竞争缓解策略。例如,只有真正发生修改动作时才临时获取相应范围内的独占权限,其余时候允许其他线程自由执行查询请求[^2]。 --- #### 扩容过程的不同之处 最后值得一提的是两者的动态扩展行为也有很大差别: - 当触发条件满足时,`HashMap` 会对整张表格进行全面复制迁移,并重新计算每项记录的目标槽位地址[^4]; - 反观 `ConcurrentHashMap` ,由于预先划分好了若干分区单元的缘故,所以只需单独考虑那些受到影响的小块内容就够了 。而且为了进一步加快速度,新版本引入了所谓 “转移队列”的概念,允许多个 worker 并行参与这项耗时任务[^2]。 ```java // 示例代码展示如何初始化两个容器对象 Map<String, String> map = new HashMap<>(); ConcurrentMap<String, String> concurrentMap = new ConcurrentHashMap<>(); ``` --- 问题
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值