当前位置: 首页 > news >正文

JUC(8)线程安全集合类

1. 概述

2. ConcurrentHashMap

1. 使用

  1. ConcurrentHashMap单个方法是线程安全的,但是对同一个集合的两个方法组合并不是原子的。所以需要注意线程安全
  2. 比如: 对get()和put()方法的原子性:
    1. 提供了一个computeIfAbsent()这个API来保证两个方法的原子性..

注:当key不存在的时候,mappingFunction方法的结果会作为值直接插入到map中,无需在mappingFunction中再执行put


线程安全版:

2. 原理

3. 源码分析

1. 构造器分析

2. get()流程

全程是没有加锁的,典型的弱一致性的体现
get()方法中所有的涉及的共享变量都定义成volatile类型,保证读取的都是最新值。根据Java内存模型提供的happends-before规则,规定对volatile变量的读一定发生在写之后,且写的最新数据对读是可见的,这是volatile替换锁的经典场景

扩: cas + volatile的应用

  • 原子整数
  • 原子引用
  • 原子数组
  • 原子累加器
  • ConcurrentHashMap的get()
3. put()流程

以下数组简称(table),链表简称(bin

while(ture){
if(当前还未初始化)[CAS]初始化哈希表时使用了cas的方式,保证只会初始化一次.无需synchronized
if(当前数组下标元素为空)[CAS]添加链表头结点时使用了cas,无需synchronized
if(有别的线程正在进行扩容.)[CAS]扩容过程中如果同时put()是会出问题的,在扩容过程中完成了链表的转移后会将链表的头结点设置为forwardNode,且hash值为负数..作为标记.  注意:扩容时以链表为单位,所以协助扩容的时候锁住一个未被转移的链表,就能保证线程安全!eg: 线程1正在进行扩容,将扩容完成的链表头部标志为forwardNode作为标识,线程2正在put()时发现了forwardNode说明此时有线程正在进行扩容,线程2会帮助扩容.锁住一条链表,帮助扩容...以上判断条件解决线程安全都是通过cas的方式,并没有加锁.只有当桶下标冲突的时候,才采用synchronized来加锁..(如下)
if(当前数组下标有元素,桶下标发生了冲突)[synchronized]锁住链表的头结点然后进行链表的遍历来put.当释放锁之后,会对链表做一些优化,决定树化(8 64) / 扩容(阈值)..

4. size计数

其实就是使用了LongAdder的思想。

ConcurrentHashMap因为多线程下,所以计数不会完全准确,这也体现了其缺点"弱一致性"

5. 扩容

以链表为单位,一个链表一个链表来迁移

  1. 如果链表为空,说明处理完了,将其处理为forwardingNode
  2. 如果已经是fwd了,说明当前的链表已经转移结束,处理下一个链表
  3. 走到这里说明 链表有元素,则进行对哈希桶的上锁(synchronized)操作来锁住链表保证线程安全 ,里面还会判断是普通节点还是树节点(hashcode>0 是普通,hashcode<0 是树)
6. ConcurrentHashMap1.7和1.8的区别
JDK1.7 中的 ConcurrentHashMap

JDK1.7 中的 ConcurrentHashMap 是由 Segment 数组结构和 HashEntry 数组结构组成,即 ConcurrentHashMap 把哈希桶数组切分成小数组(Segment ),每个小数组有 n 个 HashEntry 组成。

如下图所示,首先将数据分为一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一段数据时,其他段的数据也能被其他线程访问,实现了真正的并发访问。

整个 ConcurrentHashMap 由一个个 Segment 组成,Segment 代表”部分“或”一段“的意思,所以很多地方都会将其描述为分段锁

简单来说就是,ConcurrentHashMap 是一个 Segment数组Segment 通过继承 ReentrantLock 来进行加锁,所以每次需要加锁的操作锁住的是一个 Segment,这样只要保证每个 Segment 是线程安全的,也就实现了全局的线程安全。

Segment

Segment 本身就相当于一个 HashMap 对象。

同 HashMap 一样,Segment 包含一个 HashEntry 数组,数组中的每一个 HashEntry 既是一个键值对,也是一个链表的头节点。

单一的 Segment 结构如下:

concurrencyLevel:并行级别、并发数、Segment 数,怎么翻译不重要,理解它。默认是 16,也就是说默认情况下 ConcurrentHashMap 有 16 个 Segments,所以理论上,这个时候,最多可以同时支持 16 个线程并发写,只要它们的操作分别分布在不同的 Segment 上。这个值可以在初始化的时候设置为其他值,但是一旦初始化以后,它是不可以扩容的。最大不超过 MAX_SEGMENTS 也就是2^16

默认是 16 个,共同保存在一个名为 segments 的数组当中。 因此整个ConcurrentHashMap 的结构如下:

可以说,ConcurrentHashMap 是一个二级哈希表。在一个总的哈希表下面,有若干个子哈希表。

这样的二级结构,和数据库的水平拆分有些相似。

JDK1.8 中的 ConcurrentHashMap

虽然 JDK1.7 中的 ConcurrentHashMap 解决了 HashMap 并发的安全性,但是当冲突的链表过长时,在查询遍历的时候依然很慢

在 JDK1.8 中,HashMap 引入了红黑二叉树设计,当冲突的链表长度大于 8时,会将链表转化成红黑二叉树结构,红黑二叉树又被称为平衡二叉树,在查询效率方面,又大大的提高了不少。

在数据结构上, JDK1.8 中的 ConcurrentHashMap 选择了与 HashMap 相同的数组+链表+红黑树结构;在锁的实现上,抛弃了原有的 Segment 分段锁,采用 CAS + synchronized 实现更加细粒度的锁。

将锁的级别控制在了更细粒度的哈希桶数组元素级别,也就是说只需要锁住这个链表头节点或者红黑树的根节点,就不会影响其他的哈希桶数组元素的读写,大大提高了并发度。

我们再来看看 JDK1.8 中 ConcurrentHashMap 的整体结构,内容如下:

并发度:

  • 1.7 维护了一个segment数组,数组默认容量为16.
    • 这个容量初始化指定后就不能改变了,并且不是懒惰初始化
    • segment数组中每个segment节点都维护了一个哈希表(数组+链表)
  • 1.8 因为锁的是哈希桶,所以并发度随着扩容而一起增大
总结:

虽然 HashMap 在多线程环境下操作不安全,但是在 java.util.concurrent 包下,java 为我们提供了 ConcurrentHashMap 类,保证在多线程下 HashMap 操作安全!

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

但是 JDK1.7 中,HashMap 容易因为冲突链表过长,造成查询效率低,所以在 JDK1.8 中,HashMap 引入了红黑树特性,当冲突链表长度大于8时,会将链表转化成红黑二叉树结构。

在 JDK1.8 中,与此对应的 ConcurrentHashMap 也是采用了与 HashMap 类似的存储结构,但是 JDK1.8 中 ConcurrentHashMap 并没有采用分段锁的策略,而是在元素的节点上采用 CAS + synchronized 操作来保证并发的安全性,源码的实现比 JDK1.7 要复杂的多。

3. LinkedBlockingQueue

初始化链表的时候:

head last dummy哨兵节点。注意: head last并不是真实存在的节点,而是头结点 和 尾节点的引用

加一个元素:

出队的时候,会将自己指向自己,帮助GC安全的来回收

加锁分析:

LinkedBlockingQueue采用了两把锁 和 dummy哨兵节点提高效率

哨兵节点的体现:

  • 当不存在哨兵节点的 时候,如果只有一个正常节点,那么两把锁锁的也就是同一个对象
  • 如果有哨兵节点+1个正常节点,也就是正常的两把锁锁住的是两个对象

put()分析(生产者)

take()类似(消费者)

和ArrayBlockingQueue的比较

因为Array底层是数组,不能像链表一个,单独在头和尾设置两把锁,只能设置一把锁,所以性能上
是地狱Linked的,生产者和消费者不能同时并行执行

4. ConcurrentLinkedQueue

生产者向队尾添加元素的操作 和 消费者向对头删除元素的操作,都是通过CAS的方式来实现线程安全

5. CopyOnWriteArrayList

特点:

  1. 读写并发
  2. 读读并发
  3. 写写互斥

底层实现采用了 写时复制技术(CopyOnWrite) 的思想,增删改会将原先数组进行拷贝,增删改的操作是在新的数组上操作,而读取操作都是在原先数组上,写操作完成后覆盖掉旧数组.所以读写分离,读写并发

add():
会对写的操作使用synchronized进行互斥,但是不阻塞读的操作

所有的读操作,是没有加锁的

适合于: [读多写少]的场景!典型用空间换取的线程安全

问题:
1. get弱一致性问题.(带Concurrent开头的都存在该问题)

2. 迭代器弱一致性问题

不要觉得弱一致性就不好,其实很好,它不仅做到了读读的并发,还做到了读写的并发,MVCC也是弱一致性的表现。并发高 和 一致性是矛盾的,需要权衡

后续内容:disrupt、guava、异步编程、非共享模型、并行编程

http://www.dtcms.com/a/394140.html

相关文章:

  • springboot中@EnableAsync有什么作用
  • Spark专题-第二部分:Spark SQL 入门(6)-算子介绍-Generate
  • C#练习题——Dictionary
  • Feign
  • SPA小说集之三《森林城市反甩锅战:ERP的权责边界》
  • Qt(模态对话框和非模态对话框)
  • 【无标题】物联网 frid卡控制
  • 【LLM LangChain】 模型绑定工具+调用工具(手动调用/LangGraph/AgentExecutor)+相关注意事项
  • 图神经网络(GNN)入门:用PyG库处理分子结构与社会网络
  • 【C++】编码表 STL简介:STL是什么,版本,六大组件,重要性以及学习方法总结
  • show_interrupts函数的进一步解析及irq_desc结构体
  • Kafka面试精讲 Day 19:JVM调优与内存管理
  • 10.vector容器
  • Linux系统介绍
  • MFC中的CMFCDynamicLayout类的介绍
  • UniScene 统一驾驶场景 | 生成语义占据 | 生成多视角视频 | 生成激光点云 CVPR2025
  • Git 简明教程:从原理到实战
  • 【设计模式】中介者模式
  • nginx添加modsecurity插件
  • 代码上传Github:SSH法
  • 【iOS】AFNetworking初步了解及使用
  • JVM实战-G1参数调优
  • 超简单的视频分割脚本
  • 基于51单片机电子钟闹钟12/24小时制LCD显示( proteus仿真+程序+设计报告+讲解视频)
  • 在 Windows 系统上安装官方 Codex CLI 教程
  • Redis 配置与优化全攻略
  • 二分查找左右边界写法
  • Oracle体系结构-Large Pool详解
  • Elasticsearch面试精讲 Day 19:磁盘IO与存储优化
  • 【AI智能体】Dify 搭建数据分析应用实战操作详解