并发List、Set、ConcurrentHashMap底层原理

news2024/12/26 23:28:25

并发List、Set、ConcurrentHashMap底层原理

image-20240218223633770

ArrayList:

List特点:元素有放入顺序,元素可重复

存储结构:底层采用数组来实现

public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable
  • Cloneable:

支持拷贝:实现Cloneable接口,重写clone方法、方法内容默认调用父类的clone方法

浅拷贝

​ 基础类型的变量拷贝之后是独立的,不会随着原变量变动而改变

String类型拷贝之后也是独立的

引用类型拷贝的是引用地址,拷贝前后的变量引用同一个堆中的对象

public Object clone() throws CloneNotSupportedException {
    Study s = (Study) super.clone();
    return s;
}

深拷贝

  • 深拷贝是创建一个新的对象,并将当前对象的非静态字段复制到该新对象,如果字段是值类型的,那么对该字段执行逐位复制,如果字段是引用类型的,那么复制引用并指向一个新的对象,而不再是原有的对象。
  • 简单来说,深拷贝就是两个对象不共享内部状态,一个对象的修改不会影响到另一个对象。

ArrayList实现了深拷贝

    public Object clone() {
        try {
            ArrayList<?> v = (ArrayList<?>) super.clone();
            v.elementData = Arrays.copyOf(elementData, size);
            v.modCount = 0;
            return v;
        } catch (CloneNotSupportedException e) {
            // this shouldn't happen, since we are Cloneable
            throw new InternalError(e);
        }
    }
  • Serialilzable

    • 序列化:将对象状态转换为可保持或传输的个数的过程
  • AbstractList

    • 继承了AbstractList,说明它是一个列表,有用相应的增、删、查、改等功能
  • List

    • 为什么继承了AbstractList还需要实现List接口
      • 在StackOverFlow 中:传送门 得票最高的答案的回答者说他问了当初写这段代码的 Josh Bloch,得知这就是一个写法错误。

基本属性

//序列化版本号(类文件签名),如果不写会默认生成,类内容的改变会影响签名变化,导致反序列化失败
private static final long serialVersionUID = 8683452581122892189L;

//如果实例化时未指定容量,则在初次添加元素时会进行扩容使用此容量作为数组长度
private static final int DEFAULT_CAPACITY = 10;

//static修饰,所有的未指定容量的实例(也未添加元素)共享此数组,两个空的数组有什么区别呢? 就是第一次添加元素时知道该 elementData 从空的构造函数还是有参构造函数被初始化的。以便确认如何扩容。空的构造器则初始化为10,有参构造器则按照扩容因子扩容
private static final Object[] EMPTY_ELEMENTDATA = {};
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
transient Object[] elementData; // arrayList真正存放元素的地方,长度大于等于size

总结之EMPTY_ELEMENTDATA和DEFAULTCAPACITY_EMPTY_ELEMENTDATA的区别:EMPTY_ELEMENTDATA是为了优化创建ArrayList空实例时产生不必要的空数组,使得所有ArrayList空实例都指向同一个空数组。DEFAULTCAPACITY_EMPTY_ELEMENTDATA是为了确保无参构成函数创建的实例在添加第一个元素时,*最小的容量*是默认大小10。

添加元素 - 默认尾部添加

效率比较高

指定下标添加元素

public void add(int index, E element) {
    rangeCheckForAdd(index);//下标越界检查
    ensureCapacityInternal(size + 1);  //同上  判断扩容,记录操作数
    //依次复制插入位置及后面的数组元素,到后面一格,不是移动,因此复制完后,添加的下标位置和下一个位置指向对同一个对象
    System.arraycopy(elementData, index, elementData, index + 1,
                     size - index);
    elementData[index] = element;//再将元素赋值给该下标
    size++;

时间复杂度为O(n),与移动的元素个数正相关

扩容:

private void grow(int minCapacity) {
    int oldCapacity = elementData.length;//获取当前数组长度
    int newCapacity = oldCapacity + (oldCapacity >> 1);//默认将扩容至原来容量的 1.5 倍
    if (newCapacity - minCapacity < 0)//如果1.5倍太小的话,则将我们所需的容量大小赋值给newCapacity
        newCapacity = minCapacity;
    if (newCapacity - MAX_ARRAY_SIZE > 0)//如果1.5倍太大或者我们需要的容量太大,那就直接拿 newCapacity = (minCapacity > MAX_ARRAY_SIZE) ? Integer.MAX_VALUE : MAX_ARRAY_SIZE 来扩容
        newCapacity = hugeCapacity(minCapacity);
    elementData = Arrays.copyOf(elementData, newCapacity);//然后将原数组中的数据复制到大小为 newCapacity 的新数组中,并将新数组赋值给 elementData。

迭代器 iterator

public Iterator<E> iterator() {
    return new Itr();
}
private class Itr implements Iterator<E> {
    int cursor;       // 代表下一个要访问的元素下标
    int lastRet = -1; // 代表上一个要访问的元素下标
    int expectedModCount = modCount;//代表对 ArrayList 修改次数的期望值,初始值为 modCount
    //如果下一个元素的下标等于集合的大小 ,就证明到最后了
    public boolean hasNext() {
        return cursor != size;
    }
    @SuppressWarnings("unchecked")
    public E next() {
        checkForComodification();//判断expectedModCount和modCount是否相等,ConcurrentModificationException
        int i = cursor;
        if (i >= size)//对 cursor 进行判断,看是否超过集合大小和数组长度
            throw new NoSuchElementException();
        Object[] elementData = ArrayList.this.elementData;
        if (i >= elementData.length)
            throw new ConcurrentModificationException();
        cursor = i + 1;//自增 1。开始时,cursor = 0,lastRet = -1;每调用一次next方法,cursor和lastRet都会自增1。
        return (E) elementData[lastRet = i];//将cursor赋值给lastRet,并返回下标为 lastRet 的元素
    }
    public void remove() {
        if (lastRet < 0)//判断 lastRet 的值是否小于 0
            throw new IllegalStateException();
        checkForComodification();//判断expectedModCount和modCount是否相等,ConcurrentModificationException
        try {
            ArrayList.this.remove(lastRet);//直接调用 ArrayList 的 remove 方法删除下标为 lastRet 的元素
            cursor = lastRet;//将 lastRet 赋值给 curso
            lastRet = -1;//将 lastRet 重新赋值为 -1,并将 modCount 重新赋值给 expectedModCount。
            expectedModCount = modCount;
        } catch (IndexOutOfBoundsException ex) {
            throw new ConcurrentModificationException();
        }
    }
    final void checkForComodification() {
        if (modCount != expectedModCount)
            throw new ConcurrentModificationException();
    }

remove方法的弊端

  • 只能进行remove操作,add、clear等Iterator中没有
  • 调用remove之前必须调用next。因为remove开始就对lastRet做了校验。而lastRet初始化时为-1
  • next之后只可以调用一次remove。因为remove会将lastRet重新初始化为-1

什么是fail-fast

fail-fast机制是java集合中的一种错误机制。

当使用迭代器迭代时,如果发现集合有修改,则快速失败做出响应,抛出ConcurrentModificationException异常。

这种修改有可能是其它线程的修改,也有可能是当前线程自己的修改导致的,比如迭代的过程中直接调用remove()删除元素等。

另外,并不是java中所有的集合都有fail-fast的机制。比如,像最终一致性的ConcurrentHashMap、CopyOnWriterArrayList等都是没有fast-fail的。

fail-fast是怎么实现的:

ArrayList、HashMap中都有一个属性modcount,每次对集合的修改这个值都会加1,在遍历前记录这个值expert*count中,遍历中检查两者是否一致,如果出现不一致就说明有修改,则抛出ConcurrentModificationException异常。

底层数组存/取元素效率非常的高(get/set),时间复杂度是O(1),而查找(比如:indexOf,contain),插入和删除元素效率不太高,时间复杂度为O(n)。

插入/删除元素会触发底层数组频繁拷贝,效率不高,还会造成内存空间的浪费,解决方案:linkedList

查找元素效率不高,解决方案:HashMap(红黑树)

LinkedList

存储结构:底层采用链表来实现

HashSet(Set):

特点:元素无放入顺序,元素不可重复(注意:元素虽然无放入顺序,但是元素在set中的位置是有该元素的HashCode决定的,其位置其实是固定的)

存储结构

底层采用HashMap来实现

HashMap(Map):

特点:key,value存储,key可以为null,同样的key会被覆盖掉

存储结构:底层采用数组、链表、红黑树来实现

原理讲解:

哈希算法(也叫散列),就是把任意长度值(Key)通过散列算法变换成固定长度的key(地址)通过这个地址进行访问的数据结构它通过把关键码值映射到表中一个。位置来访问记录,以加快查找的速度。

image-20240219230658004

image-20240219230720082

链表查询的时候链表过长查询效率非常低,所以需要红黑树

JDK8中的HashMap与JDK7中的HashMap有什么不同
  • JDK8中新增了红黑树,JDK8是通过数组 + 链表 + 红黑树来实现的
  • JDK7中链表的插入是用的头插法,而JDK8中则改为了尾插法
  • JDK8中因为使用了红黑树保证了插入和查询的效率,所以实际上JDK8中的Hash算法实现的复杂度降低了
  • JDK8中数组扩容的条件也发生了变化,只会判断是否当前元素的个数是否查过了阈值,而不再判断当前put进来的元素对应的数组小标位置是否有值
  • JDK7是先扩容再添加新的元素、JDK8是先添加新元素然后再扩容
HashMap中put方法的流程
  • 通过key计算出一个hashcode
  • 通过hashcode与"与操作"计算出一个数组下标
  • 在把put进来的key、value封装成一个entry对象
  • 判断数组下标对应的位置,是不是为空,如果是空则把entry直接存在改数组位置
  • 如果改下标对应的位置不为空,则需要把entry插入到链表中
  • 并且还需要判断改链表中是否存在相同的key,如果存在,则更新value
  • 如果是JDK7使用头插法
  • 如果是JDK8,则会遍历链表,遍历链表的过程中,统计当前链表的元素个数,如果超过8个,则先把链表转变为红黑树、并且把元素插入到红黑树中
JDK中链表转变为红黑树的条件
  • 链表中的元素的个数为8个或超过8个
  • 同时,还需要满足当前数组的长度大于或等于64才会把链表转变为红黑树
    • 因为链表转变为红黑树的目的是为了解决链表过长,导致查询和插入效
      率慢的问题,而如果要解决这个问题,也可以通过数组扩容,把链表缩短也可
      以解决这个问题。所以在数组长度还不太长的情况,可以先通过数组扩容来解
      决链表过长的问题
HashMap扩容流程是怎样的?
  • HashMap的扩容指的就是数组的扩容, 因为数组占用的是连续内存空间,所以数组的扩容其实只能新开一个新的数组,然后把老数组上的元素转移到新
    数组上来,这样才是数组的扩容
  • 在HashMap中也是一样,先新建一个2被数组大小的数组
  • 然后遍历老数组上的没一个位置,如果这个位置上是一个链表,就把这个链表上的元素转移到新数组上去
  • 在这个过程中就需要遍历链表,当然jdk7,和jdk8在这个实现时是有不一样的,jdk7就是简单的遍历链表上的没一个元素,然后按每个元素的hashcode结合新数组的长度重新计算得出一个下标,而重新得到的这个数组下标很可能和之前的数组下标是不一样的,这样子就达到了一种效果,就是扩容之后,某个链表会变短,这也就达到了扩容的目的,缩短链表长度,提高了查询效率
  • 而在jdk8中,因为涉及到红黑树,这个其实比较复杂,jdk8中其实还会用到一个双向链表来维护红黑树中的元素,所以jdk8中在转移某个位置上的元素
    时,会去判断如果这个位置是一个红黑树,那么会遍历该位置的双向链表,遍历双向链表统计哪些元素在扩容完之后还是原位置,哪些元素在扩容之后在新位置,这样遍历完双向链表后,就会得到两个子链表,一个放在原下标位置,一个放在新下标位置,如果原下标位置或新下标位置没有元素,则红黑树不用拆分,否则判断这两个子链表的长度,如果超过八,则转成红黑树放到对应的位置,否则把单向链表放到对应的位置
  • 元素转移完了之后,在把新数组对象赋值给HashMap的table属性,老数组会被回收到
为什么HashMap的数组的大小是2的幂次方

JDK7的HashMap是数组+链表实现的,JDK8的HashMap是数组+链表+红黑树实现的

当某个key-value对需要存储到数组中时,需要先生成一个数组下标index,并且这个index不能越界。

在HashMap中,先得到key的hashcode,hashcode是一个数字,然后通过hashcode & (table.length - 1) 运算得到一个数组下标index,是通过与运算计算出来一个数组下标的,而不是通过取余,与运算相比于取余运算速度更快,但是也有一个前提条件,就是数组的长度得是一个2的幂次方数。

ConcurrentHashMap

特点:并发安全的HashMap,比HashTable效率更高

存储结构:底层采用数组、链表、红黑树、内部大量采用CAS操作。并发控制使用synchronized和CAS来操作来实现的。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1457569.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【医学大模型 补全主诉】BioGPT + LSTM 自动补全医院紧急部门主诉

BioGPT LSTM 自动补全医院紧急部门主诉 问题&#xff1a;针对在紧急部门中自动补全主诉的问题子问题1: 提高主诉记录的准确性子问题2: 加快主诉记录的速度子问题3: 统一医疗术语的使用子问题4: 减少打字错误和误解子问题5: 提高非特定主诉的处理能力 解法数据预处理神经网络方…

选择结构switch

一、执行流程 所有case都和表达式的值不匹配&#xff0c;就会执行default语句体部分 从被匹配的位置开始执行&#xff0c;如果遇到break&#xff0c;那么退出选择结构 二、注意事项 1、case后面的【常量值】不能重复&#xff0c;不然编译器会报错 2、switch后面的小括号只…

数字化转型导师坚鹏:政府数字化转型之数字化技术

政府数字化转型之数字化技术 ——物联网、云计算、大数据、人工智能、虚拟现实、区块链、数字孪生、元宇宙等综合解析及应用 课程背景&#xff1a; 数字化背景下&#xff0c;很多政府存在以下问题&#xff1a; 不清楚新技术的发展现状&#xff1f; 不清楚新技术的重要应…

las数据转pcd数据

las数据转pcd数据 一、算法原理1.介绍las2.主要函数 二、代码三、结果展示3.1 las数据3.2 las转换为pcd 四、相关数据链接 一、算法原理 1.介绍las LAS文件按每条扫描线排列方式存放数据,包括激光点的三维坐标、多次回波信息、强度信息、扫描角度、分类信息、飞行航带信息、飞…

【已解决】d:\recording\2A327.mp3 (拒绝访问。)

在用JAVA程序&#xff0c;下载音频到本地文件夹的时候&#xff0c;显示拒绝访问。 一开始我以为是文件夹没有权限&#xff0c;但是在我赋予了写入权限后&#xff0c;仍然出现拒绝访问的提示。 我观察了一下&#xff0c;保存到本地的时候&#xff0c;多写了一层文件夹。因为到这…

【Git】:初识git

初识git 一.创建git仓库二.管理文件三.认识.git内部结构 一.创建git仓库 1.安装git 使用yum install git -y即可安装git。 2.创建仓库 首先创建一个git目录。 3.初始化仓库 这里面有很多内容&#xff0c;后面会将&#xff0c;主要是用来进行追踪的。 4.配置name和email 当然也…

WEB服务器的超级防护——安全WAF

随着网络和信息技术的不断发展&#xff0c;特别是互联网的广泛普及和应用&#xff0c;网络正在逐步改变人类的生活和工作方式。越来越多的政府和企业组织建立了依赖于网络的业务信息系统&#xff0c;例如电子政务、网络办公等。网络也对社会各行各业产生了巨大的影响&#xff0…

【Java基础知识(1)】对象在内存中的存储

Java对象是如何在内存中存储的&#xff1f; 在Java中&#xff0c;所有的对象被动态地分配在堆中。这和C不同&#xff0c;C允许对象被存储在堆或者栈中。 参考Oracle官方文档&#xff1a;官方文档说明 在Java中创建字符串对象的2种方法&#xff1a; 1、 使用String关键字&am…

Uiautomator2实现Android自动化测试详解

目录 1、UIautomator2框架原理 2、UIautomator2使用 2.1、安装 2.2、元素定位工具-weditor 2.3、设备连接 2.4、全局配置 2.4.1、通过settings设置 2.4.2、通过属性设置 2.5、APP相关操作 2.5.1、安装应用 2.5.2、启动应用 2.5.3、等待应用启动 2.5.4、结束应用 …

【机器学习笔记】 15 机器学习项目流程

机器学习的一般步骤 数据清洗 数据清洗是指发现并纠正数据文件中可识别的错误的最后一道程序&#xff0c;包括检查数据一致性&#xff0c;处理无效值和缺失值等。与问卷审核不同&#xff0c;录入后的数据清理一般是由计算机而不是人工完成。 探索性数据分析(EDA 探索性数据…

从gradient_checkpointing_enable中学习

1.背景 最近在使用官网的教程训练chatGLM3,但是出现了“RuntimeError: element 0 of tensors does not require grad and does not have a grad_fn”错误,查阅了官方的文档,目前这个问题还没什么解决方案 但是其中有人回复说:是注释掉503行的model.gradient_checkpointing_e…

手持三防平板丨国产化加固平板丨国产三防平板发展的意义是什么?

随着现代科技的快速发展&#xff0c;平板电脑在我们的生活中扮演着越来越重要的角色。然而&#xff0c;传统的平板电脑只能在普通的环境中使用&#xff0c;而无法在恶劣的环境中使用&#xff0c;例如在高海拔、高温、高湿度、沙漠等环境中&#xff0c;传统平板电脑往往会出现故…

OpenHarmony—UIAbility组件间交互(设备内)

UIAbility是系统调度的最小单元。在设备内的功能模块之间跳转时&#xff0c;会涉及到启动特定的UIAbility&#xff0c;该UIAbility可以是应用内的其他UIAbility&#xff0c;也可以是其他应用的UIAbility&#xff08;例如启动三方支付UIAbility&#xff09;。 本章节将从如下场…

电脑数据丢失怎么办?5 种免费数据恢复软件能帮到您

我们存储在计算机中的个人和专业数据如果丢失&#xff0c;可能会给我们带来经济和精神上的困扰。有许多情况会导致此类数据丢失&#xff1b;其中一些包括意外删除、硬盘驱动器故障、软件崩溃、病毒攻击等。 5 种最佳免费数据恢复软件 为防止此类事故&#xff0c;建议定期备份计…

智慧城市驿站:智慧公厕升级版,打造现代化城市生活的便捷配套

随着城市化进程的加速&#xff0c;人们对城市生活质量的要求也越来越高。作为智慧城市建设的一项重要组成部分&#xff0c;多功能城市智慧驿站应运而生。它集合了信息技术、设计美学、结构工艺、系统集成、环保节能等多个亮点&#xff0c;将现代科技与城市生活相融合&#xff0…

RK3568平台 有线以太网接口之MAC芯片与PHY芯片

一.平台网络网络通路 平台有线以太网通路&#xff1a;有线以太网一般插入的是RJ45 座要与 PHY 芯片&#xff08;RTL8306M&#xff09;连接在一起&#xff0c;但是中间需要一个网络变压器&#xff0c;网络变压器经过模数转换后到达网卡(RTL8111)转换为帧数据后到达SOC。 二.网络…

【初始RabbitMQ】交换机的实现

交换机概念 RabbitMQ消息传递模型的核心思想就是&#xff1a;生产者生产的消息从不会直接发送到队列。实际上&#xff0c;通常生产者不知道这些消息会传递到那些队列中 相反&#xff0c;生产者只能将消息发送到交换机&#xff0c;交换机的工作内容也很简单&#xff0c;一方面…

Kubernetes基础(二十二)-k8s持久化存储详解

1 volume 1.1 介绍 在容器中的磁盘文件是短暂的&#xff0c;当容器崩溃时&#xff0c;Kubelet会重新启动容器&#xff0c;但容器运行时产生的数据文件都将会丢失&#xff0c;之后容器会以最干净的状态启动。另外&#xff0c;当一个Pod运行多个容器时&#xff0c;各个容器可能…

科技守护大唐遗宝,预防保护传承千年

​ 一、“大唐遗宝——何家村窖藏出土文物展” 陕西历史博物馆的“唐朝遗宝——何家村窖藏出土文物展”算得上是博物馆展览的典范。展览不仅在于展现了数量之多、等级之高、种类之全&#xff0c;更在于对唐朝历史文化的深入揭露。 走入大唐财产展厅&#xff0c;好像穿越千年前…

【Azure 架构师学习笔记】- Azure Databricks (7) --Unity Catalog(UC) 基本概念和组件

本文属于【Azure 架构师学习笔记】系列。 本文属于【Azure Databricks】系列。 接上文 【Azure 架构师学习笔记】- Azure Databricks (6) - 配置Unity Catalog 前言 在以前的Databricks中&#xff0c;主要由Workspace和集群、SQL Warehouse组成&#xff0c; 这两年Databricks公…