Interview系列 - 07 Java | 集合的快速失败和安全失败机制 | 迭代器类源码 | CopyOnWriteArrayList

news/2024/5/3 18:02:21/文章来源:https://blog.csdn.net/qq_42764468/article/details/129193185

文章目录

      • 1. 集合的快速失败 (fail-fast)
        • 1. 使用增强for遍历集合并使用ArrayList的 remove() 方法删除集合元素
        • 2. 使用 forEach 遍历集合并使用ArrayList的 remove() 方法删除集合元素
        • 3. 使用迭代器遍历集合并使用ArrayList的 remove() 方法删除集合元素
        • 4. 使用迭代器遍历集合并使用迭代器的 remove() 方法删除元素
        • 5. 源码分析
      • 2. 同步容器
      • 3. 集合的安全失败机制 (fail-safe)
      • 4. CopyOnWriteArrayList
        • 1. CopyOnWriteArrayList的使用
        • 2. CopyOnWriteArrayList的原理
        • 3. CopyOnWriteArrayList源码分析
        • 4. CopyOnWriteArrayList的优点
        • 5. CopyOnWriteArrayList和ReentrantReadWriteLock的比较

请思考以下几个问题:
1、遍历一个 List 有哪些不同的方式?实现原理是什么?Java 中 List遍历的最佳实践是什么?
2、如何边遍历边删除集合元素?
3、什么是集合的快速失败机制?
4、多线程场景下如何使用 ArrayList?

1. 集合的快速失败 (fail-fast)

采用快速失败机制的集合容器,使用迭代器进行遍历集合时,除了通过迭代器自身的 remove() 方法之外,对集合进行任何其他方式的结构性修改,则会抛出 ConcurrentModificationException 异常。

1. 使用增强for遍历集合并使用ArrayList的 remove() 方法删除集合元素

public class Main {public static void main(String[] args) {List<String> initList = Arrays.asList("张三", "李四", "周一", "刘四", "李强", "李白");List<String> list = new ArrayList(initList);for (String element : list) {if (element.startsWith("李")) {// 利用ArrayList的remove()方法对集合进行修改list.remove(element);}}System.out.println(list);}
}

在这里插入图片描述
抛出并发修改异常!其实,for(xx in xx) 就是增强的 for循环,即迭代器 Iterator 的加强实现,其内部是调用的 Iterator 的方法。

2. 使用 forEach 遍历集合并使用ArrayList的 remove() 方法删除集合元素

public class Main {public static void main(String[] args) {List<String> initList = Arrays.asList("张三", "李四", "周一", "刘四", "李强", "李白");List<String> list = new ArrayList(initList);list.forEach((e) -> {if (e.contains("李")) {list.remove(e);}});System.out.println(list);}
}

在这里插入图片描述
forEach 方法的背后其实就是增强的 for 循环,底层即迭代器,所以使用 list.remove 同样抛出 ConcurrentModificationException 异常。

3. 使用迭代器遍历集合并使用ArrayList的 remove() 方法删除集合元素

public class Main {public static void main(String[] args) {List<String> initList = Arrays.asList("张三", "李四", "周一", "刘四", "李强", "李白");List<String> list = new ArrayList(initList);for (Iterator<String> ite = list.iterator(); ite.hasNext(); ) {String str = ite.next();if (str.contains("李")) {// 利用ArrayList的remove()方法对集合进行修改list.remove(str);}}System.out.println(list);}
}

在这里插入图片描述
又是那个并发修改异常,这个示例虽然使用了 Iterator 循环,但删除的时候却使用了 list.remove 方法,同样是有问题的。

4. 使用迭代器遍历集合并使用迭代器的 remove() 方法删除元素

public class Main {public static void main(String[] args) {List<String> initList = Arrays.asList("张三", "李四", "周一", "刘四", "李强", "李白");List<String> list = new ArrayList(initList);for (Iterator<String> iterator = list.iterator(); iterator.hasNext(); ) {String str = iterator.next();if (str.contains("李")) {iterator.remove();}}System.out.println(list);}
}

结果输出正常,这是因为迭代器中的 remove 方法将期待修改的数量(expectedModCount)值进行了同步。

5. 源码分析

public class ArrayList<E> extends AbstractList<E>implements List<E>, RandomAccess, Cloneable, java.io.Serializable{// 1、添加元素方法public boolean add(E e) {ensureCapacityInternal(size + 1);  // Increments modCount!!elementData[size++] = e;return true;}private void ensureCapacityInternal(int minCapacity) {ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));}private void ensureExplicitCapacity(int minCapacity) {// 快速失败机制:迭代器内部维护了一个 modCount 变量,添加元素时,modCount++modCount++;if (elementData.length < minCapacity)grow(minCapacity);}// 2、删除元素方法public E remove(int index) {rangeCheck(index);// 快速失败机制:迭代器内部维护了一个 modCount 变量,添加元素时,modCount++modCount++;E oldValue = elementData(index);int numMoved = size - index - 1;if (numMoved > 0)System.arraycopy(elementData, index+1, elementData, index,numMoved);elementData[--size] = null; // clear to let GC do its workreturn oldValue;}// 3、迭代器方法public Iterator<E> iterator() {return new Itr();}// 4、ArrayList中的内部类private class Itr implements Iterator<E> {int cursor;       // index of next element to returnint lastRet = -1; // index of last element returned; -1 if no suchint expectedModCount = modCount;Itr() {}public boolean hasNext() {return cursor != size;}@SuppressWarnings("unchecked")public E next() {// 每次调动迭代器的next()方法都会检查集合元素是否修改,如果修改了modCount的值会改变checkForComodification();int i = cursor;if (i >= size)throw new NoSuchElementException();Object[] elementData = ArrayList.this.elementData;if (i >= elementData.length)throw new ConcurrentModificationException();cursor = i + 1;return (E) elementData[lastRet = i];}public void remove() {if (lastRet < 0)throw new IllegalStateException();// 每次调动迭代器的remove()方法都会检查集合元素是否修改,如果修改了modCount的值会改变checkForComodification();try {ArrayList.this.remove(lastRet);cursor = lastRet;lastRet = -1;// 使用迭代器的remove()方法,内部在调用ArrayList的remove()方法后,会同步修改expectedModCount,所以不会发生并发修改异常!expectedModCount = modCount;} catch (IndexOutOfBoundsException ex) {throw new ConcurrentModificationException();}}final void checkForComodification() {// 当检测到 modCount != expectedmodCount 时,抛出异常if (modCount != expectedModCount)throw new ConcurrentModificationException();}}
}

迭代器在遍历时直接访问集合的内容时,集合中的内容在遍历的过程中无法被修改。为了保证不被修改,迭代器内部维护了一个 modCount 变量 ,当集合结构改变(添加、删除),就会改变 modCount 的值。每当迭代器使用 next() 方法遍历下一个元素时,都会检查 modCount 的值是否等于 expectedmodCount 的值,当检测到 modCount != expectedmodCount 时,抛出 ConcurrentModificationException 异常,反之继续遍历。

2. 同步容器

根据上面的分析可见,ArrayList集合单线程下效率相对较高,多线程环境下,线程不安全。 java.util包下的集合类都是快速失败的,不能在多线程下发生并发修改。

如果想要保证线程安全,可以使用线程安全的同步容器,即使用sychronized关键字来实现线程安全的容器,比如 Vector,Collections工具类中的同步方法。

3. 集合的安全失败机制 (fail-safe)

采用安全失败机制的集合容器,使用迭代器进行遍历时不是直接在集合内容上访问的,而是将原有集合内容进行拷贝,在拷贝的集合上进行遍历。

迭代器在遍历时访问的是拷贝的集合,所以在遍历过程中对原集合所作的修改并不能被迭代器检测到,所以不会触发 ConcurrentModificationException 异常。

优缺点:

(1) 由于对集合进行了拷贝,避免了 ConcurrentModificationException 异常,但拷贝时产生大量的无效对象,开销大。

(2) 无法保证读取到的数据是原集合中最新的数据,即迭代器进行遍历的是拷贝的集合,在遍历期间原集合发生的修改,迭代器是检测不到的。

(3) java.util.concurrent包下的容器都是安全失败,可以在多线程下并发使用,并发修改。

4. CopyOnWriteArrayList

1. CopyOnWriteArrayList的使用

Collections可以将基础容器包装为线程安全的同步容器,但是这些同步容器包装类在进行元素迭代时并不能进行元素添加操作。

public class CopyOnWriteArrayListTest {//并发操作的执行目标public static class CocurrentTarget implements Runnable {//并发操作的目标队列List<String> list = null;public CocurrentTarget(List<String> targetList) {this.list = targetList;}@Overridepublic void run() {Iterator<String> iterator = list.iterator();//迭代操作while (iterator.hasNext()) {// 在迭代操作时,进行列表的修改String threadName = Thread.currentThread().getName();System.out.println("开始往同步队列加入线程名称:" + threadName);list.add(threadName);}}}//测试同步队列:在迭代操作时,进行列表的修改public static void main(String[] args) throws InterruptedException {List<String> notSafeList = Arrays.asList("a", "b", "c");List<String> synList = Collections.synchronizedList(notSafeList);//创建一个执行目标CocurrentTarget synchronizedListListDemo = new CocurrentTarget(synList);//10个线程并发for (int i = 0; i < 10; i++) {new Thread(synchronizedListListDemo, "线程" + i).start();}//主线程等待Thread.sleep(1000);}
}

在这里插入图片描述
那么,该如何解决此问题呢?可使用CopyOnWriteArrayList替代Collections.synchronizedList同步包装实例:

public class CopyOnWriteArrayListTest {//并发操作的执行目标public static class CocurrentTarget implements Runnable {//并发操作的目标队列List<String> list = null;public CocurrentTarget(List<String> targetList) {this.list = targetList;}@Overridepublic void run() {Iterator<String> iterator = list.iterator();//迭代操作while (iterator.hasNext()) {// 在迭代操作时,进行列表的修改String threadName = Thread.currentThread().getName();System.out.println("开始往同步队列加入线程名称:" + threadName);list.add(threadName);}}}//测试同步队列:在迭代操作时,进行列表的修改public static void main(String[] args) throws InterruptedException {List<String> notSafeList = Arrays.asList("a", "b", "c");List<String> copyOnWriteArrayList = new CopyOnWriteArrayList(notSafeList);//创建一个执行目标CocurrentTarget cocurrentTarget = new CocurrentTarget(copyOnWriteArrayList);//10个线程并发for (int i = 0; i < 10; i++) {new Thread(cocurrentTarget, "线程" + i).start();}//主线程等待Thread.sleep(1000);}
}

使用CopyOnWriteArrayList容器可以在进行元素迭代的同时进行元素添加操作。那么CopyOnWriteArrayList是如何做到的呢?

2. CopyOnWriteArrayList的原理

CopyOnWrite(写时复制)就是在修改器对一块内存进行修改时,不直接在原有内存块上进行写操作,而是将内存复制一份,在新的内存中进行写操作,写完之后,再将原来的指针(或者引用)指向新的内存,原来的内存被回收。

CopyOnWriteArrayList是写时复制思想的一种典型实现,其含有一个指向操作内存的内部指针array,而可变操作(add、set等)是在array数组的副本上进行的。当元素需要被修改或者增加时,并不直接在array指向的原有数组上操作,而是首先对array进行一次复制,将修改的内容写入复制的副本中。写完之后,再将内部指针array指向新的副本,这样就可以确保修改操作不会影响访问器的读取操作。

在这里插入图片描述
CopyOnWriteArrayList是一个满足CopyOnWrite思想并使用Array数组存储数据的线程安全List。

3. CopyOnWriteArrayList源码分析

(1) CopyOnWriteArrayList的属性:

public class CopyOnWriteArrayList<E>implements List<E>, RandomAccess, Cloneable, java.io.Serializable {private static final long serialVersionUID = 8673264195747942595L;/** 对所有的修改器方法进行保护,访问器方法并不需要保护 */final transient ReentrantLock lock = new ReentrantLock();/** 内部对象数组,通过 getArray/setArray方法访问 */private transient volatile Object[] array;//获取内部对象数组final Object[] getArray() {return array;}// 设置内部对象数组final void setArray(Object[] a) {array = a;}// 省略其他代码
}

(2) CopyOnWriteArrayList的读取操作:

访问器的读取操作没有任何同步控制和锁操作,理由是内部数组array不会发生修改,只会被另一个array替换,因此可以保证数据安全。

/** 操作内存的引用*/
private transient volatile Object[] array;public E get(int index) {return get(getArray(), index);
}//获取元素
@SuppressWarnings("unchecked")
private E get(Object[] a, int index) {return (E) a[index];
}//返回操作内存
final Object[] getArray() {return array;
}

(3) CopyOnWriteArrayList的写入操作:

CopyOnWriteArrayList的写入操作add()方法在执行时加了独占锁以确保只能有一个线程进行写入操作,避免多线程写的时候会复制出多个副本。

// 在集合的末尾添加元素
public boolean add(E e) {final ReentrantLock lock = this.lock;lock.lock();  // 加锁try {Object[] elements = getArray();int len = elements.length;// 复制新数组Object[] newElements = Arrays.copyOf(elements, len + 1);  newElements[len] = e;setArray(newElements);return true;} finally {lock.unlock();  // 释放锁}
}// 在集合的指定位置添加指定元素
public void add(int index, E element) {final ReentrantLock lock = this.lock;lock.lock();try {Object[] elements = getArray();int len = elements.length;if (index > len || index < 0)throw new IndexOutOfBoundsException("Index: "+index+", Size: "+len);Object[] newElements;int numMoved = len - index;if (numMoved == 0)newElements = Arrays.copyOf(elements, len + 1);else {newElements = new Object[len + 1];System.arraycopy(elements, 0, newElements, 0, index);System.arraycopy(elements, index, newElements, index + 1,numMoved);}newElements[index] = element;setArray(newElements);} finally {lock.unlock();}
}

从add()操作可以看出,在每次进行添加操作时,CopyOnWriteArrayList底层都是重新复制一份数组,再往新的数组中添加新元素,待添加完了,再将新的array引用指向新的数组。当add()操作完成后,array的引用就已经指向另一个存储空间了。

既然每次添加元素的时候都会重新复制一份新的数组,那就带来了一个问题,就是增加了内存的开销,如果容器的写操作比较频繁,那么其开销就比较大。所以,在实际应用的时候,CopyOnWriteArrayList并不适合进行添加操作。但是在并发场景下,迭代操作比较频繁,CopyOnWriteArrayList就是一个不错的选择。

(4) CopyOnWriteArrayList的迭代器实现:

CopyOnWriteArray有自己的迭代器,该迭代器不会检查修改状态,也无须检查状态。为什么呢?因为被迭代的array数组可以说是只读的,不会有其他线程能够修改它。

//获取迭代器
public Iterator<E> iterator() {return new COWIterator<E>(getArray(), 0);
}//返回操作内存
final Object[] getArray() {return array;
}static final class COWIterator<E> implements ListIterator<E> {/**对象数组的快照(snapshot)*/private final Object[] snapshot;/** Index of element to be returned by subsequent call to next.  */private int cursor;private COWIterator(Object[] elements, int initialCursor) {cursor = initialCursor;snapshot = elements;}public boolean hasNext() {return cursor < snapshot.length;}//下一个元素public E next() {if (! hasNext())throw new NoSuchElementException();return (E) snapshot[cursor++];}
}

4. CopyOnWriteArrayList的优点

CopyOnWriteArrayList有一个显著的优点,那就是读取、遍历操作不需要同步,速度会非常快。所以,CopyOnWriteArrayList适用于读操作多、写操作相对较少的场景(读多写少),比如可以在进行“黑名单”拦截时使用CopyOnWriteArrayList。

5. CopyOnWriteArrayList和ReentrantReadWriteLock的比较

CopyOnWriteArrayList和ReentrantReadWriteLock读写锁的思想非常类似,即读读共享、写写互斥、读写互斥、写读互斥。但是前者相比后者的更进一步:为了将读取的性能发挥到极致,CopyOnWriteArrayList读取是完全不用加锁的,而且写入也不会阻塞读取操作,只有写入和写入之间需要进行同步等待,读操作的性能得到大幅度提升。

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

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

相关文章

【离线数仓-6-数据仓库开发ODS层设计要点】

离线数仓-6-数据仓库开发ODS层设计要点离线数仓-6-数据仓库开发ODS层1.数据仓库开发ODS层设计要点2.ODS层用户行为日志表1.hive中复杂结构体复习1.array2.map3.struct 复杂结构4.嵌套格式2.hive中针对复杂结构字符串的练习1.针对ods层为json格式数据的练习2.用户行为日志表的设…

详解数据库基本概念

数据库&#xff08;DataBase 简称 DB&#xff09;&#xff1a;是一个长期存储在计算机内的、有组织的、可共享的、统一管理的大量数据的集合数据库管理系统&#xff08;DataBase Management System 简称 DBMS&#xff09;&#xff1a;是一种操纵和管理数据库的大型软件&#xf…

获取Windows11开发环境及VirtualBox配置指南

今天我们来讲一讲Windows11开发环境的快速搭建&#xff0c;主要是通过Virtualbox虚拟机安装微软官方预先配置好的Windows11环境包&#xff0c;配置简单&#xff0c;开箱即用。 获取虚拟机打包镜像 微软官方提供了多个系统平台的Windows11虚拟机镜打包镜像&#xff0c;只需要导…

python--排序总结

1.快速排序 a.原理 快速排序的基本思想是在待排序的 n 个元素中任取一个元素&#xff08;通常取第一个元素&#xff09;作为基准&#xff0c;把该元素放人最终位置后&#xff0c;整个数据序列被基准分割成两个子序列&#xff0c;所有小于基准的元素放置在前子序列中&#xff0…

代码随想录算法训练营day40 | 动态规划 343. 整数拆分 96.不同的二叉搜索树

day40343. 整数拆分1、确定dp数组以及下标的含义2、确定递推公式3、dp数组如何初始化4、确定遍历顺序5、举例推导dp数组96.不同的二叉搜索树1、确定dp数组&#xff08;dp table&#xff09;以及下标的含义2、确定递推公式3、dp数组如何初始化4、确定遍历顺序5、举例推导dp数组3…

[译文] 基于PostGIS3.1 生成格网数据

根据格网进行数据统计与分析是一种常用的方法&#xff0c;相比自然地理边界与行政管理边界而言&#xff0c;使用格网有如下特点&#xff1a;每个格网之间地位相等&#xff0c;没有上下级之分。每个格网的面积都相等。相邻两个格网单元中心点之间距离相等。适用于将数据从“空间…

【Kubernetes 企业项目实战】09、Rancher 2.6 管理 k8s-v1.23 及以上版本高可用集群

目录 一、Rancher 介绍 1.1Rancher简介 1.2 Rancher 和 k8s 的区别 1.3 Rancher 企业使用案例 二、安装 Rancher 2.1 初始化环境 2.2 安装 Rancher 2.3 登录 Rancher 平台 三、通过 Rancher 管理已存在的 k8s 集群 3.1 配置 rancher 3.2 导入 k8s ​四、通过 Ranc…

【MySQL】5.7版本解压安装配置

前言 之所以使用解压版本&#xff0c;而不使用exe安装&#xff0c;因为exe的安装方式删除过于麻烦&#xff01;&#xff01;&#xff01; 如果安装MySQL过程中&#xff0c;出错了或者想重新在来一把&#xff0c;删除mysql服务即可 sc delete mysql # 删除已经安装好的Mysql&a…

ICASSP2023录用率有可靠度还不错的消息了

点击文末公众号卡片&#xff0c;找对地方&#xff0c;轻松参会 由于录用邮件没说录用率&#xff0c;导致大家都不知道录用率是多少。 据一位群友的反馈&#xff0c;其小老板是meta review。该群友原话“接受率应该是42%”。 ICASSP2023投稿量6000&#xff0c;在投稿量大涨的…

可怕,chatGPT用3小时教会我数据

chatGPT这玩意真的是我的救星,用它作为我的Python教练,我用三个小时学会了数据处理(Pandas)和绘图(matplotlib)。 这两个库的学习,在之前已经困扰了我7个月。之前卡壳的原因,是我一直没有耐心从零开始,按照教材设置的教程去学习Python——我擅长在项目中学习,一点一点…

数字人文中的可视化

数字人文中的可视化罗煜楚1&#xff0c;吴昊1&#xff0c;郭宇涵1&#xff0c;谭绍聪1&#xff0c;刘灿1&#xff0c;蒋瑞珂1&#xff0c;袁晓如1,21北京大学智能学院机器感知与智能教育部重点实验室&#xff0c;北京 1008712北京大学大数据分析与应用技术国家工程实验室&#…

白帽黑客入行应该怎么学?零基础小白也能轻松上手!

这几年随着我国《国家网络空间安全战略》《网络安全法》《网络安全等级保护2.0》等一系列政策/法规/标准的持续落地&#xff0c;网络安全行业地位、薪资随之水涨船高。 1为什么网络安全行业是IT行业最后的红利&#xff1f; 根据腾讯安全发布的《互联网安全报告》&#xff0c;…

【架构师】零基础到精通——网关策略

博客昵称&#xff1a;架构师Cool 最喜欢的座右铭&#xff1a;一以贯之的努力&#xff0c;不得懈怠的人生。 作者简介&#xff1a;一名退役Coder&#xff0c;软件设计师/鸿蒙高级工程师认证&#xff0c;在备战高级架构师/系统分析师&#xff0c;欢迎关注小弟&#xff01; 博主小…

月薪没到30K的测试员必须要会的技能,我先啃为敬

最近感慨面试难的人越来越多了&#xff0c;一方面是市场环境&#xff0c;更重要的一方面是企业对软件测试的人才要求越来越高了。 基本上这样感慨的分为两类人 第一&#xff0c;虽然挂着3、5年经验&#xff0c;但肚子里货少&#xff0c;也没啥拿得出手的项目&#xff0c;自己还…

整数保序的离散化(C/C++)

目录 1. 离散化的概念 1.1 离散化的运用思路 1.2 离散化的方法 1.2.1 排序 1.2.2 确定一个元素离散化后的结果 1.3 案例分析 1.3.1 1.3.2 区间和 &#xff08;来源&#xff1a;Acwing&#xff09; 1. 离散化的概念 离散化&#xff0c;把无限空间中有限的个体映射到有限的…

用kinectv2运行orbslam2

前提 vim 、 cmake 、 git 、 gcc 、 g 这些一般都装了 主要是Pangolin 、 OpenCV 、 Eigen的安装 18.04建议Pangolin0.5 orbslam2安装、测试&#xff1a; git clone https://github.com/raulmur/ORB_SLAM2.git ORB_SLAM2 cd ORB_SLAM2 chmod x build.sh ./build.sh 编译…

归并排序及其应用

归并排序算法基于分而治之的概念&#xff0c;具体来说就是遍历一棵树&#xff0c;归并的过程是一个后序执行的动作。 由于我们知道每个子部分在合并后都是有序的&#xff0c;我们可以利用这个特性来解决一些问题。 上图可视化了merge sort algorithm的过程&#xff0c;我们很容…

【Spring中@Autowired和@Resource注解的区别?】

一.背景 Spring中Autowired和Resource注解的区别&#xff1f; Spring框架想必大家都知道吧&#xff0c;那么Spring中Autowired和Resource注解的区别你知道吗&#xff1f;如果不知道也不要紧&#xff0c;我们就一起来学习一起吧。 二.Autowired和Resource注解的区别&#xff1f…

【Azure 架构师学习笔记】-Azure Data Factory (3)-触发器详解-翻转窗口

本文属于【Azure 架构师学习笔记】系列。 本文属于【Azure Data Factory】系列。 接上文【Azure 架构师学习笔记】-Azure Data Factory (2)-触发器 前言 上文中提到触发器的类型有以下4种&#xff0c;其中第一种【计划】是常用的&#xff0c; 与其他工具/服务类似的方式&#…

电路分析:一个简单的光控灯电路

一个简单的光控灯电路 电路原理&#xff1a; 利用了光敏电阻、电容 、三极管的特性实现