ConcurrentHashMap 底层原理和JDK版本对比

文章目录

概要

ConcurrentHashMap是懒加载,并不会在创建对象的时候进行初始化,只有第一次添加数据的时候才会进行初始化。

线程进来首先会判断当前对象是否为空,判断是否需要进行初始化。

使用了大量的CAS操作来进行并发控制,这种操作在多线程环境下能够有效地实现原子性更新。CAS 的引入使得在进行插入、更新和删除操作时,减少了锁的使用,提高了性能。

当链表长度超过一定阈值时,会将链表转换为红黑树,以提高查找和删除操作的性能。

提供了线程安全的 putIfAbsent() 方法,它可以在键不存在时插入一个键值对。这个方法的原子性操作可以在多线程环境中避免重复插入。

自动扩容:ConcurrentHashMap 会根据负载因子(load factor)来自动扩容,以保证数据的均衡分布和高效的查找性能。

CAS 操作:ConcurrentHashMap 使用了 CAS(Compare-And-Swap)等原子性操作来保证线程安全性。这些操作能够在不加锁的情况下进行数据的更新。

高并发环境优化:ConcurrentHashMap 在高并发环境下表现优秀,特别适合读多写少的情况,可以显著减少锁竞争,提高性能。

适用场景:ConcurrentHashMap 适用于需要在多线程环境下进行并发访问的场景,如并发缓存、并行计算等。

功能丰富的 API:ConcurrentHashMap 提供了丰富的 API,包括线程安全的插入、删除、查找操作,以及诸如 forEach、reduce 等方法,支持各种并发操作需求。

数据结构

JDK版本对比

JDK1.7中的底层数据结构。

        底层一个Segments数组;数组(Segment)+ 链表

JDK1.8中数据结构

        Node数组+链表 / 红黑树, 类似HashMap

Node数组使用来存放树或者链表的头结点,当一个链表中的数量到达一个数目时,会使查询速率降低,所以到达一定阈值时,会将一个链表转换为一个红黑二叉树,提高查询的速率。

ConcurrentHashMap相比HashMap而言,是多线程安全的,其底层数据与HashMap的数据结构相同。

类的构造函数
    public ConcurrentHashMap() {
    }

说明:该构造函数用于创建一个带有默认初始容量 (16)、加载因子 (0.75) 和 concurrencyLevel (16) 的新的空映射。

扩容机制

触发扩容的有三种场景

第一:在链表转红黑树的时候进行判断,如果此时没有达到阈值,则不会转成红黑树,会对数组进行扩容。

第二:进行putAll()方法的时候,可能会触发扩容机制,因为putAll传入的参数是map对象,如果这个map的数据比较多,就会进行扩容操作。

第三:添加元素的时候,如果达到阈值,会进行扩容机制。

技术名词解释

我们对以下技术名词解释

名词举例:

  • CAS

        CAS是Compare-and-Swap的缩写,也叫比较交换。它是一种原子性操作,用于多线程编程中同步访问共享资源的问题。CAS操作需要同时传递一个期望值和一个新值,它会比较当前共享资源的值是否等于期望值,如果相等则把共享资源的值设置为新值,否则不做任何修改,并返回当前的共享资源的值。

  • 红黑树

        红黑树是一种自平衡二叉查找树,是一种数据结构,典型的用途是实现关联数组。这种结构确保了红黑树在任何时候都是接近平衡的,从而在最坏情况下,其查找、插入和删除操作的时间复杂度都是O(log n)。红黑树通过一系列的规则和操作(如颜色更改和节点旋转)来维护这些性质,从而保持树的平衡。

小结

1、在 JDK1.7 中,ConcurrentHashMap 采用了分段锁策略,将一个 HashMap 切割成 Segment 数组,其中 Segment 可以看成一个 HashMap, 不同点是 Segment 继承自 ReentrantLock,在操作的时候给 Segment 赋予了一个对象锁,从而保证多线程环境下并发操作安全。

ConcurrentHashMap采用了数组+Segment+分段锁的方式实现。ConcurrentHashMap定位一个元素的过程需要进行两次Hash操作。
第一次Hash定位到Segment,第二次Hash定位到元素所在的链表的头部。

2、在JDK1.8中,取消了segment数组,锁的粒度更小,减少并发冲突的概率。原来是对需要进行数据操作的Segment加锁,现调整为对每个数组元素加锁(Node)。JDK1.8中的 ConcurrentHashMap 与 HashMap 非常相似,只是 ConcurrentHashMap 中增加了同步的操作和 CAS 来实现并发操作。采用了synchronized+CAS+数组+链表+红黑树的实现方式来设计。

3、JDK1.8存储数据时采用了链表+红黑树的形式,纯链表的形式时间复杂度为O(n),红黑树则为O(logn),性能提升很大。什么时候链表转红黑树?当key值相等的元素形成的链表中元素个数超过8个的时候。
4、JDK1.8版本的数据结构变得更加简单,使得操作也更加清晰流畅,因为已经使用synchronized来进行同步,所以不需要分段锁的概念,也就不需要Segment这种数据结构了,由于粒度的降低,实现的复杂度也增加了。
5、JDK1.8使用红黑树来优化链表,基于长度很长的链表的遍历是一个很漫长的过程,而红黑树的遍历效率是很快的,代替一定阈值的链表,这样形成一个性能的极大提升。
6、JDK1.8为什么使用内置锁synchronized来代替重入锁ReentrantLock?

 (1)锁的粒度

首先锁的粒度并没有变粗,甚至变得更细了。每当扩容一次,ConcurrentHashMap的并发度就扩大一倍。

(2)Hash冲突

JDK1.7中,ConcurrentHashMap从过二次hash的方式(Segment -> HashEntry)能够快速的找到查找的元素。在1.8中通过链表加红黑树的形式弥补了put、get时的性能差距。
JDK1.8中,在ConcurrentHashmap进行扩容时,其他线程可以通过检测数组中的节点决定是否对这条链表(红黑树)进行扩容,减小了扩容的粒度,提高了扩容的效率。

从以上两个角度分析,得以说明原因:

(1)减少内存开销

假设使用可重入锁来获得同步支持,那么每个节点都需要通过继承AQS来获得同步支持。但并不是每个节点都需要获得同步支持的,只有链表的头节点(红黑树的根节点)需要同步,这无疑带来了巨大内存浪费。

(2)获得JVM的支持

可重入锁毕竟是API这个级别的,后续的性能优化空间很小。
synchronized则是JVM直接支持的,JVM能够在运行时作出相应的优化措施:锁粗化、锁消除、锁自旋等等。这就使得synchronized能够随着JDK版本的升级而不改动代码的前提下获得性能上的提升。

7、JDK8中的实现也是锁分离思想,只是锁住的是一个node,而不是JDK7中的Segment;锁住Node之前的操作是基于在volatile和CAS之上无锁并且线程安全的。

相关推荐

  1. ConcurrentHashMap 底层原理JDK版本对比

    2024-03-10 07:50:02       41 阅读
  2. React底层原理分析(简单大白话版本

    2024-03-10 07:50:02       50 阅读
  3. jdk1.8 ConcurrentHashMap 源码分析

    2024-03-10 07:50:02       30 阅读

最近更新

  1. docker php8.1+nginx base 镜像 dockerfile 配置

    2024-03-10 07:50:02       98 阅读
  2. Could not load dynamic library ‘cudart64_100.dll‘

    2024-03-10 07:50:02       106 阅读
  3. 在Django里面运行非项目文件

    2024-03-10 07:50:02       87 阅读
  4. Python语言-面向对象

    2024-03-10 07:50:02       96 阅读

热门阅读

  1. [axios]使用指南

    2024-03-10 07:50:02       45 阅读
  2. 【Vue3 组合式 API: reactive 和 ref 函数】

    2024-03-10 07:50:02       43 阅读
  3. 【conda】conda卸载并重新安装指定版本软件package

    2024-03-10 07:50:02       44 阅读
  4. 用C语言easyx 做一个《正弦彩环》

    2024-03-10 07:50:02       41 阅读
  5. python知网爬虫论文pdf下载+立即可用(动态爬虫)

    2024-03-10 07:50:02       40 阅读
  6. PHP端口批量查询工具单文件

    2024-03-10 07:50:02       46 阅读
  7. 动态SLAM论文阅读笔记

    2024-03-10 07:50:02       52 阅读