ArrayList源码分析
ArrayList源码分析
- 目标:
- 一、 ArrayList的简介
- 二、ArrayList原理分析
- 2.1 ArrayList的数据结构源码分析
- 2.2 ArrayList默认容量&最大容量
- 2.3 为什么ArrayList查询快,增删慢?
- 2.4 ArrayList初始化容量
- 1、创建ArrayList对象分析:无参数
- 2、创建ArrayList对象分析:带有初始化容量构造方法
- 2.5 ArrayList扩容原理
- 三、ArrayList线程安全问题及解决方案
- 3.1 错误复现
- 3.2 导致ArrayList线程不安全的源码分析
- 3.3 解决方案
- 四、ArrayList的Fail-Fast机制深入理解
目标:
- 理解ArrayList的底层数据结构
- 深入掌握ArrayList查询快,增删慢的原因
- 掌握ArrayList的扩容机制
- 掌握ArrayList初始化容量过程
- 掌握ArrayList出现线程安全问题原因及解决方案
- 掌握ArrayList的Fail-Fast机制
一、 ArrayList的简介
ArrayList
集合是Collection
和List
接口的实现类。底层的数据结构是数组。数据结构特点 : 增删慢,查询快。线程不安全的集合!
许多程序员开发的时候,使用集合基本上无脑选取ArrayList!不建议这种用法。
ArrayList的特点:
- 单列集合 : 对应与Map集合来说【双列集合】
- 有序性 : 存入的元素和取出的元素是顺序是一样的
- 元素可以重复 : 可以存入两个相同的元素
- 含带索引的方法 : 数组与生俱来含有索引【下角标】
二、ArrayList原理分析
2.1 ArrayList的数据结构源码分析
//空的对象数组
private static final Object[] EMPTY_ELEMENTDATA = {};
//默认容量空对象数组,通过空的构造参数生成ArrayList对象实例
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
//ArrayList对象的实际对象数组!
transient Object[] elementData; // non-private to simplify nested class access
//1、为什么是Object类型呢?利用面向对象的多态特性,当前ArrayList的可以存储任意引用数据类型。
//2、ArrayList有一个问题,不能存储基本数据类型!就是数组的类型是Object类型
2.2 ArrayList默认容量&最大容量
//默认的初始化容量是10
private static final int DEFAULT_CAPACITY = 10;
//最大容量 : 2^31 - 1 - 8 = 21 4748 3639【21亿】
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
为什么最大容量要-8呢?
目的是为了存储ArrayList集合的基本信息,比如list集合的最大容量!
2.3 为什么ArrayList查询快,增删慢?
ArrayList
的底层数据结构就是一个Object数组
,一个可变的数组,对于其的所有操作都是通过数组来实 现的。
- 数组是一种,查询快、增删慢!
- 查询数据是通过索引定位,查询任意数据耗时均相同。查询效率贼高!
- 删除数据时,要将原始数据删除,同时后面的每个数据迁移。删除效率就比较低!
- 新增数据,在添加数组的位置加入数组,同时在数组后面位置后移以为!添加效率极低!
2.4 ArrayList初始化容量
ArrayList底层是数组,动态数组!
- 底层是Object对象数组,数组存储的数据类型是Object,数组名字为elementData。
transient Object[] elementData; // non-private to simplify nested class access
1、创建ArrayList对象分析:无参数
创建ArrayList的之后,ArrayList容量是多少呢?回答10是错误的!回答0是正确【限定条件,在
JDK1.8中】
如何 初始化 动态数组的容量?10个
构造方法
/*** Constructs an empty list with an initial capacity of ten. */
//空参构造时,创建一个空数组
public ArrayList() {this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
//空数组!
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
在执行add()方法的时候初始化!【懒加载】
判断当前数组的容量是否有存储空间,如果没有初始化一个10的容量。
//向数组中,添加一个元素
public boolean add(E e) {//确保有容量,如果第一次添加,会初始化一个容量为10的list //size当前集合元素的个数,随着添加的元素递增ensureCapacityInternal(size + 1); // Increments modCount!! //添加元素elementData[size++] = e; return true;
}
//ensureCapacityInternal确保有容量,如果第一次添加,会初始化一个容量为10的list
private void ensureCapacityInternal(int minCapacity) {//两个方法ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}
// calculateCapacity(elementData, minCapacity) 拿着当前ArrayList的数组,与当前数组中的元素个数。计算容量
private static int calculateCapacity(Object[] elementData, int minCapacity)
{//ArrayList的数组与默认的数组进行比较。 //{} == {}if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {/true //DEFAULT_CAPACITY = 10//minCapacity 1 //1和10比谁大 10return Math.max(DEFAULT_CAPACITY, minCapacity);//计算之后,返回的初始化容量是10}return minCapacity;
}
// ensureExplicitCapacity() 确保不会超过数组的真实容量
private void ensureExplicitCapacity(int minCapacity) { //minCapacity 当前计算后容量 10modCount++;//对当前数组操作计数器 // overflow-conscious code//最小的容量: 10 - 当前数组的容量{} 0if (minCapacity - elementData.length > 0) grow(minCapacity);//做了扩容
}
2、创建ArrayList对象分析:带有初始化容量构造方法
//创建ArrayList集合,并且设置固定的集合容量
public ArrayList(int initialCapacity) { //initialCapacity 手动设置的初始化容量if (initialCapacity > 0) {//判断容量是否大于0,如果大于0 //创建一个对象数组位指定容量大小,并且交给ArrayList对象 this.elementData = new Object[initialCapacity]; //如果设置的容量为0,设置默认数组} else if (initialCapacity == 0) {this.elementData = EMPTY_ELEMENTDATA;//默认的元素数据数组{} } else {//如果不是0,也不是大于0的数,会抛出非法参数异常!throw new IllegalArgumentException("Illegal Capacity: "+initialCapacity);}
}
注意 : 使用ArrayList的集合,建议如果知道集合的大小,最好提前设置。提示集合的使用效率!
2.5 ArrayList扩容原理
add方法先要确保数组的容量足够,防止数组已经填满还往里面添加数据造成数组越界:
- 如果数组空间足够,直接将数据添加到数组中
- 如果数组空间不够了,则进行扩容。扩容1.5倍扩容。
- 扩容 : 原始数组copy新数组中,同时向新数组后面加入数据
注意 : new的ArrayList的对象没有容量的,在第一次添加的add,会进行第一次扩容。0 -> 10!
//grow扩容数组
private void grow(int minCapacity) {//minCapacity 当前数组的最小容量,存储了多少个元素 // overflow-conscious code//获取当前存储数据数组的长度int oldCapacity = elementData.length;//新的容量 = 旧的容量 + 扩容的容量【旧容量/2 = 0.5旧容量】 //扩容1.5倍扩容int newCapacity = oldCapacity + (oldCapacity >> 1); //极端情况过滤 : 新的容量 - 旧的容量小于0【int值移除】 if (newCapacity - minCapacity < 0)newCapacity = minCapacity;//不扩容了 //新的容量,比ArrayList的最大值,还要大if (newCapacity - MAX_ARRAY_SIZE > 0)//设置新的容量为ArrayList的最大值,以ArrayList最大值为当前容量 newCapacity = hugeCapacity(minCapacity);// minCapacity is usually close to size, so this is a win: elementData = Arrays.copyOf(elementData, newCapacity);
}
总结:
- 扩容的规则并不是翻倍,是原来容量的1.5倍
- ArrayList的数组最大值Integer.MAX_VALUE。不允许超过这个最大值
- 新增元素时,没有严格的数据值的检查。所有可用设置null
三、ArrayList线程安全问题及解决方案
3.1 错误复现
ArrayList 我们都知道底层是以数组方式实现的,实现了可变大小的数组,它允许所有元素,包括null。
看下面一个例子:开启多个线程操作List集合,向ArrayList中增加元素,同时去除元素
//全局线程共享集合ArrayListprotected static ArrayList<Object> arrayList = new ArrayList<>();@Testvoid arrayListTest() {//1.创建线程数组【500】Thread[] threads = new Thread[500];//2.遍历数组,向线程中添加500个线程对象for (int i = 0; i < threads.length; i++) {threads[i] = new MyThread();threads[i].start();//启动线程}//3.遍历线程,等待线程执行完毕【等待所有线程执行完毕】for (int i = 0; i < threads.length; i++) {try {threads[i].join();//等待线程执行完毕} catch (InterruptedException e) {e.printStackTrace();}}//线程执行内容 : 向集合中添加自己的线程名称//4.遍历list集合,获取所有线程的名称for (Object threadName : arrayList) {System.out.println("threadName = " + threadName);}}//线程执行内容,是想集合中添加自己的线程名称class MyThread extends Thread {@Overridepublic void run() {try {//线程休眠1000Thread.sleep(1000);//向集合中添加自己的线程名称【操作共享内容,会出现线程安全问题】arrayList.add(Thread.currentThread().getName());} catch (InterruptedException e) {e.printStackTrace();}}}
运行代码结果可知,会出现以下几种情况:
- ①打印null
- ②某些线程并未打印
- ③数组角标越界异常
3.2 导致ArrayList线程不安全的源码分析
ArrayList成员变量
public class ArrayList<E> extends AbstractList<E>implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{//ArrayList的Object的数组存所有元素。transient Object[] elementData; // non-private to simplify nested class
access//size变量保存当前数组中元素个数。 private int size;
//...
}
- ArrayList的Object的数组存储所有元素。
- size变量保存当前数组中元素个数。
出现线程不安全源码之一 : add()方法
public boolean add(E e) {//确保有容量,如果第一次添加,会初始化一个容量为10的list //size当前集合元素的个数,随着添加的元素递增ensureCapacityInternal(size + 1); // Increments modCount!! //添加元素elementData[size++] = e; return true;
}
add添加元素,实际做了两个大的步骤:
- 判断elementData数组容量是否满足需求
- 在elementData对应位置上设置值
线程不安全的隐患【1】,导致③数组下标越界异常
线程不安全的隐患【2】,导致①Null、②某些线程并未打印
致①Null、②某些线程并未打印深层次原因是因为多线程情况下出现了指令重排和不保证原子性问题。详见Volatile关键字
由此我们可以得出,在多线程情况下操作ArrayList 并不是线性安全的。
3.3 解决方案
第一种方案:使用Vector集合,Vector集合是线程安全的
//线程安全问题解决方案1
protected static Vector<Object> vector = new Vector<>();
第二种方案:使用Collections.synchronizedList。它会自动将我们的list方法进行改变,最后返回给我们加锁了List
//线程安全问题解决方案2
//将集合改为同步集合
protected static List<Object> synList = Collections.synchronizedList(arrayList);
第三种方案:使用JUC中的CopyOnWriteArrayList类进行替换。
//线程安全问题解决方案3 JUC 【最佳选择】
protected static CopyOnWriteArrayList<Object> copyOnWriteArrayList = new
CopyOnWriteArrayList<>();
因为CopyOnWriteArrayList中的add操作使用了lock锁保证了原子性同时保证线程安全,而且数组也使用了volatile关键字,保证可见性和防止指令重排
四、ArrayList的Fail-Fast机制深入理解
什么是Fail-Fast机制?
"快速失败"即Fail-Fast机制,它是Java中一种错误检测机制!
当多钱程对集合进行结构上的改变,或者在迭代元素时直接调用自身方法改变集合结构而没有通知迭代器时,有可能会触发Fail-Fast机制并抛出异常【ConcurrentModificationException】。注意,是有可能
触发Fail-Fast,而不是肯定!
触发时机 : 在迭代过程中,集合的结构发生改变,而此时迭代器并不知情,或者还没来得及反应,便会
产生Fail-Fast事件。
再次强调,迭代器的快速失败行为无法得到保证!一般来说,不可能对是否出现不同步并发修改,或者
自身修改做出任何硬性保证。快速失败迭代器会尽最大努力抛出 ConcurrentModificationException。
Java.util包中的所有集合类都是快速失败的,而java.util.concurrent包中的集合类都是安全失败的;快
速失败的迭代器抛出ConcurrentModificationException,而安全失败的迭代器从不抛出这个异常。
ArrayList的Fast-Fail事件复现及解决方案
/*** 目标: 复现Fast_Fail机制* 1.产生条件 :* 当多线程操作同一个集合* 同时遍历这个集合,该集合被修改!* 2.解决方案 :使用并发编程包中的集合,CopyOnWriteArrayList替换原有ArrayList集合。*///定义全局共享集合 :static ArrayList<String> list = new ArrayList<>();//Fast_Fail机制CopyOnWriteArrayList// static CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>();@Testvoid arrayListTest() {//创建线程1,并且向集合添加元素,打印集合中的内容Thread thread1 = new Thread(() -> {//并且向集合添加元素for (int i = 0; i < 6; i++) {list.add("" + i);// 打印集合中的内容printAll();}});thread1.start();//启动线程1//创建线程2,并且向集合添加元素,打印集合中的内容Thread thread2 = new Thread(() -> {//并且向集合添加元素for (int i = 10; i < 16; i++) {list.add("" + i);// 打印集合中的内容printAll();}});thread2.start();//启动线程2}/*** 使用迭代器打印集合*/public static void printAll() {//获取当前集合的迭代器Iterator<String> iterator = list.iterator();//通过迭代器遍历集合while (iterator.hasNext()) {String value = iterator.next();System.out.println(value + ",");}}
使用ArrayList在多线程情况下添加元素时出线ConcurrentModificationException
解决办法使用并发编程包中的集合,CopyOnWriteArrayList替换原有ArrayList集合。
扩展连接
ArrayList集合底层原理
ArrayList集合特点为什么是增删慢、查询快
相关文章:

ArrayList源码分析
ArrayList源码分析目标:一、 ArrayList的简介二、ArrayList原理分析2.1 ArrayList的数据结构源码分析2.2 ArrayList默认容量&最大容量2.3 为什么ArrayList查询快,增删慢?2.4 ArrayList初始化容量1、创建ArrayList对象分析:无参数2、创建A…...

SpringBoot IOC、DI、@Autowired、@Resource、作用域
一、初识Spring1.1 Spring是什么Spring是一个轻量级Java开发框架,目的是为了解决企业级应用开发的业务逻辑层和其他各层的耦合问题。它是一个分层的开源框架,为开发Java应用程序提供全面的基础架构支持。Spring负责基础架构,Java开发者可以专…...

链表相关oj题
1.Leetcode203 移除链表元素 解题思路:从头节点开始进行元素删除,每删除一个元素,需要重新链接节点 struct ListNode* removeElements(struct ListNode* head, int val){struct ListNode*dummyheadmalloc(sizeof(struct ListNode));dummyhea…...

【Linux】操作系统(Operator System)
操作系统(Operator System )一、操作系统的概念二、操作系统的作用三、系统调用和库函数一、操作系统的概念 操作系统是一组控制和管理计算机软硬件资源,为用户提供便捷使用的计算机程序的集合,是配置在计算机硬件系统上的第一层…...

机器学习自学笔记——感知机
感知机预备知识 神经元 感知机算法最初是由科学家从脑细胞的神经凸起联想而来。如下图,我们拥有三个初始xxx值,x1,x2,x0x_1,x_2,x_0x1,x2,x0。其中x01x_01x01为一个初始的常量,专业上称作“偏置”。每个xxx的值都会乘上一个权重…...
C++ Primer第五版_第三章习题答案(21~30)
文章目录练习3.21练习3.22练习3.23练习3.24练习3.25练习3.26练习3.27练习3.28练习3.29练习3.30练习3.21 请使用迭代器重做3.3.3节的第一个练习。 #include <vector> #include <iterator> #include <string> #include <iostream>using std::vector; usi…...

colmap+openmvs进行三维重建流程全记录
window下的colmapopenmvs进行三维重建流程全记录 1.colmap安装与配置 可参考:https://blog.csdn.net/weixin_44153180/article/details/129334018?spm1001.2014.3001.5501 2.openmvs安装与配置 可参考:https://blog.csdn.net/rdw1246010462/article…...

yolov8命令行运行参数详解
序言 整理来自yolov8官方文档常用的一些命令行参数,官方文档YOLOv8 Docs yolov8命令行的统一运行格式为: yolo TASK MODE ARGS其中主要是三部分传参: TASK(可选) 是[detect、segment、classification]中的一个。如果没有显式传递…...

分布式锁简介
Redis因为单进程、性能高常被用于分布式锁;锁在程序中作用是同步工具,保证共享资源在同一时刻只能被一个线程访问。 Java中经常用的锁synchronized、Lock,但是Java的锁智能保证单机的时候有效,分布式集群环境就无能为力了…...

【嵌入式Linux学习笔记】Linux驱动开发
Linux系统构建完成后,就可以基于该环境方便地进行开发了,相关的开发流程与MCU类似,但是引入了设备树的概念,编写应用代码要相对复杂一点。但是省去了很多配置工作。 学习视频地址:【正点原子】STM32MP157开发板 字符…...
2023年中国高校计算机大赛-团队程序设计天梯赛(GPLT)上海理工大学校内选拔赛(同步赛)(H题)(线段树)
又到了万物复苏的季节,家乡的苹果树结果了。像往常一样小龙同学被叫回家摘苹果。 假设需要采摘的一棵树上当前有a颗苹果,那么小龙会采摘⌈a/3⌉颗苹果,其中⌈x⌉表示不小于x的最小整数。 但是,为了可持续发展,若a小于1…...
Linux内核Thermal框架详解十三、Thermal Governor(3)
接前一篇文章Linux内核Thermal框架详解十二、Thermal Governor(2) 二、具体温控策略 上一篇文章介绍并详细分析了bang_bang governor的源码。本文介绍第2种温控策略:fair_share。 2. fair_share fair_share governor总的策略是频率档位⽐较…...
TikTok品牌出海创世纪(二)
目录 1.推荐算法打造王者品牌 2.品牌聚焦海外Z群体 3.持续扩展应用场景 加速品牌全球化传播 品牌聚焦海外Z群体 “这个地球上,三分之二的人都在用Facebook“,这是对Facebook曾经统治地位最直观的描述。 但如今,这家全球社交媒体巨头的光环正…...

iOS中SDK开发 -- cocoapods库创建
在iOS项目中,经常使用cocoadpods来进行依赖管理以及三方库引入等。引入的三方库一般会有几种形式:一、在Pods目录下可以直接看到源代码的开源库,如AFNetworking,Masonry等常见开源库。二、在Pods目录下拉取的项目文件只能看到对应…...

2023年了,还是没学会内卷....
先做个自我介绍:我,普本,通信工程专业,现在飞猪干软件测试,工作时长两年半。 回望疫情纪元,正好是实习 毕业这三年。要说倒霉也是真倒霉,互联网浪潮第三波尾巴也没抓住,狗屁造富神…...

chatGPT爆火,什么时候中国能有自己的“ChatGPT“
目录 引言 一、ChatGPT爆火 二、中国何时能有自己的"ChatGPT" 三、为什么openai可以做出chatGPT? 四、结论 引言 随着人工智能技术的不断发展,自然语言处理技术也逐渐成为了研究的热点之一。其中,ChatGPT作为一项领先的自然语言处理技术…...

【Matlab算法】粒子群算法求解一维非线性函数问题(附MATLAB代码)
MATLAB求解一维非线性函数问题前言正文函数实现(可视化处理)可视化结果前言 一维非线性函数是指函数的自变量和因变量都是一维实数,而且函数的形式是非线性的,也就是不符合线性函数的形式。在一维非线性函数中,自变量…...

2023 最新发布超全的 Java 面试八股文,整整 1000道面试题,太全了
作为一名优秀的程序员,技术面试都是不可避免的一个环节,一般技术面试官都会通过自己的方式去考察程序员的技术功底与基础理论知识。 2023 年的互联网行业竞争越来越严峻,面试也是越来越难,很多粉丝朋友私信希望我出一篇面试专题或…...

产品经理面经|当面试官问你还有什么问题?
相信很多产品经理在跳槽面试的时候,在面试尾声都会遇到这样的环节,面试官会问你有什么问题要问的,一般来说大家都能随时随地甩出几个问题来化解,但其实在这个环节对于应聘者来说也是一个很好的机会来展现自己的能力,甚…...

单链表的基本操作
目录 一.链表的基本概念和结构 二.链表的分类 三.单链表的基本操作 1.创建一个节点 2.打印 3.尾插 4.头插 5.尾删 6.头删 7.查找 8.指定位置插入 9.指定位置删除 10.销毁 一.链表的基本概念和结构 概念:链表是一种物理存储结构上非连续、非顺序的存储结…...

网络六边形受到攻击
大家读完觉得有帮助记得关注和点赞!!! 抽象 现代智能交通系统 (ITS) 的一个关键要求是能够以安全、可靠和匿名的方式从互联车辆和移动设备收集地理参考数据。Nexagon 协议建立在 IETF 定位器/ID 分离协议 (…...
k8s从入门到放弃之Ingress七层负载
k8s从入门到放弃之Ingress七层负载 在Kubernetes(简称K8s)中,Ingress是一个API对象,它允许你定义如何从集群外部访问集群内部的服务。Ingress可以提供负载均衡、SSL终结和基于名称的虚拟主机等功能。通过Ingress,你可…...

大数据零基础学习day1之环境准备和大数据初步理解
学习大数据会使用到多台Linux服务器。 一、环境准备 1、VMware 基于VMware构建Linux虚拟机 是大数据从业者或者IT从业者的必备技能之一也是成本低廉的方案 所以VMware虚拟机方案是必须要学习的。 (1)设置网关 打开VMware虚拟机,点击编辑…...

CentOS下的分布式内存计算Spark环境部署
一、Spark 核心架构与应用场景 1.1 分布式计算引擎的核心优势 Spark 是基于内存的分布式计算框架,相比 MapReduce 具有以下核心优势: 内存计算:数据可常驻内存,迭代计算性能提升 10-100 倍(文档段落:3-79…...

Keil 中设置 STM32 Flash 和 RAM 地址详解
文章目录 Keil 中设置 STM32 Flash 和 RAM 地址详解一、Flash 和 RAM 配置界面(Target 选项卡)1. IROM1(用于配置 Flash)2. IRAM1(用于配置 RAM)二、链接器设置界面(Linker 选项卡)1. 勾选“Use Memory Layout from Target Dialog”2. 查看链接器参数(如果没有勾选上面…...

Unsafe Fileupload篇补充-木马的详细教程与木马分享(中国蚁剑方式)
在之前的皮卡丘靶场第九期Unsafe Fileupload篇中我们学习了木马的原理并且学了一个简单的木马文件 本期内容是为了更好的为大家解释木马(服务器方面的)的原理,连接,以及各种木马及连接工具的分享 文件木马:https://w…...
LangFlow技术架构分析
🔧 LangFlow 的可视化技术栈 前端节点编辑器 底层框架:基于 (一个现代化的 React 节点绘图库) 功能: 拖拽式构建 LangGraph 状态机 实时连线定义节点依赖关系 可视化调试循环和分支逻辑 与 LangGraph 的深…...

嵌入式学习之系统编程(九)OSI模型、TCP/IP模型、UDP协议网络相关编程(6.3)
目录 一、网络编程--OSI模型 二、网络编程--TCP/IP模型 三、网络接口 四、UDP网络相关编程及主要函数 编辑编辑 UDP的特征 socke函数 bind函数 recvfrom函数(接收函数) sendto函数(发送函数) 五、网络编程之 UDP 用…...

二叉树-144.二叉树的前序遍历-力扣(LeetCode)
一、题目解析 对于递归方法的前序遍历十分简单,但对于一位合格的程序猿而言,需要掌握将递归转化为非递归的能力,毕竟递归调用的时候会调用大量的栈帧,存在栈溢出风险。 二、算法原理 递归调用本质是系统建立栈帧,而非…...
Ansible+Zabbix-agent2快速实现对多主机监控
ansible Ansible 是一款开源的自动化工具,用于配置管理(Configuration Management)、应用部署(Application Deployment)、任务自动化(Task Automation)和编排(Orchestration…...