并发List、Set、ConcurrentHashMap底层原理

并发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 Cloneablethrow 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倍太小的话,则将我们所需的容量大小赋值给newCapacitynewCapacity = 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是否相等,ConcurrentModificationExceptionint 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 的值是否小于 0throw new IllegalStateException();checkForComodification();//判断expectedModCount和modCount是否相等,ConcurrentModificationExceptiontry {ArrayList.this.remove(lastRet);//直接调用 ArrayList 的 remove 方法删除下标为 lastRet 的元素cursor = lastRet;//将 lastRet 赋值给 cursolastRet = -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.hqwc.cn/news/478538.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

AUTOSAR CP--chapter7从CAN网络学习Autosar通信

从CAN网络学习Autosar通信 前言缩写词CAN通信在AUTOSAR架构中的传输上位机配置 第六章总结&#xff1a;学习了如何使用工具的自动配置功能&#xff0c;位我们生成系统描述中部分ecu的BSW模块配置&#xff0c;但是自动配置的功能虽然为我们提供了极大的便利&#xff0c;我们仍然…

树与二叉树

树与二叉树 文章目录 树与二叉树一、树的概念及结构1.、树的概念2、树的相关概念1.3 树的表示 二、二叉树1.概念2、特殊的二叉树3、二叉树的性质4、二叉树的存储结构 三、二叉树的顺序结构及实现1、二叉树的顺序结构2、堆的概念及结构3、堆的实现 四、二叉树链式结构的实现1、遍…

JAVA--异常处理

目录 1. 异常概述 1.1 什么是生活的异常 1.2 什么是程序的异常 1.3 异常的抛出机制 1.4 如何对待异常 2. Java异常体系 2.1 Throwable 2.2 Error 和 Exception 2.3 编译时异常和运行时异常 3. 常见的错误和异常 3.1 Error 3.2 运行时异常 3.3 编译时异常 4. 异常…

【LeetCode每日一题】单调栈 581. 最短无序连续子数组

581. 最短无序连续子数组 给你一个整数数组 nums &#xff0c;你需要找出一个 连续子数组 &#xff0c;如果对这个子数组进行升序排序&#xff0c;那么整个数组都会变为升序排序。 请你找出符合题意的 最短 子数组&#xff0c;并输出它的长度。 示例 1&#xff1a; 输入&am…

php实现讯飞星火大模型3.5

前期准备 vscode下载安装好 composer下载安装好 php环境安装好 &#xff08;以上可以自行网上查阅资料&#xff09; 开始实现 1.注册讯飞星火用户&#xff0c;获取token使用 讯飞星火认知大模型-AI大语言模型-星火大模型-科大讯飞 2.修改对应php文件中的key等 可以参考…

FL Studio21中文版本混音功能介绍

FL Studio 21的混音功能是其音乐制作能力中不可或缺的一部分&#xff0c;它为用户提供了强大的工具&#xff0c;以便他们可以对音轨进行细致的调整&#xff0c;确保音乐作品的最终呈现效果达到最佳。 FL Studio 21 Win-安装包下载如下: https://wm.makeding.com/iclk/?zonei…

修复 Android 手机陷入恢复模式的 5 种方法

您的手机卡在 Android Recovery 模式且无法退出此模式&#xff1f;无论您按什么按钮组合&#xff0c;甚至在取出并重新插入电池后重新启动手机&#xff0c;手机都会启动回到恢复模式吗&#xff1f; Android卡在recovery模式的情况并不罕见&#xff0c;各种品牌的Android手机都…

Stable Diffusion 绘画入门教程(webui)-lora

通过前边的文章&#xff0c;相信大家已经能够自己产出好看的小姐姐或者小哥哥了&#x1f923; 不知道有没有发现每次生成的脸、身材、衣服、环境、风格等等可能都会有所差异&#xff0c;那么如果这个问题不解决&#xff0c;实用性将大大降低&#xff0c;因此lora诞生了。 文章…

unity学习(28)——登录功能

有之前注册的知识&#xff0c;登录就很容易处理了。 登陆成功返回id&#xff1a; 登录失败返回null&#xff1a; 测试同一账号不能重复登陆&#xff01;登录成功后最好可以跳到新的场景中 结果是好的&#xff0c;去服务器看一下对应部分的代码&#xff0c;可见&#xff0c;登…

单片机01天_stm32f407zg_创建新工程

创建“寄存器版工程” 1、创建工程文件夹 创建工程文件夹“Project”&#xff0c;内部包含文件夹“CMSIS”&&“USER”。 CMSIS&#xff1a;Cortex系列内核接口驱动文件。 USER&#xff1a;存放用户编写的程序文件。 “USER”文件夹内包含“Inc”&&“Src”…

ubuntu22.04@laptop OpenCV Get Started: 015_deep_learning_with_opencv_dnn_module

ubuntu22.04laptop OpenCV Get Started: 015_deep_learning_with_opencv_dnn_module 1. 源由2. 应用Demo2.1 C应用Demo2.2 Python应用Demo 3. 使用 OpenCV DNN 模块进行图像分类3.1 导入模块并加载类名文本文件3.2 从磁盘加载预训练 DenseNet121 模型3.3 读取图像并准备为模型输…

前端|Day3:CSS基础(黑马笔记)

Day3:CSS基础 目录 Day3:CSS基础一、CSS初体验二、CSS引入方式三、选择器1.标签选择器2.类选择器3.id选择器4.通配符选择器 四、盒子尺寸和背景色五、文字控制属性1.字体大小2.字体样式&#xff08;是否倾斜&#xff09;3.行高单行文字垂直居中 4.字体族5.font复合属性6.文本缩…