基于源码去理解Iterator迭代器的Fail-Fast与Fail-Safe机制

image

原创/朱季谦

在Java编程当中,Iterator迭代器是一种用于遍历如List、Set、Map等集合的工具。这类集合部分存在线程安全的问题,例如ArrayList,若在多线程环境下,迭代遍历过程中存在其他线程对这类集合进行修改的话,就可能导致不一致或者修改异常问题,因此,针对这种情况,迭代器提供了两种处理策略:Fail-Fast(快速失败)和Fail-Safe(安全失败)。

先简单介绍下这两种策略——

1. Fail-Fast(快速失败)机制
快速失败机制是指集合在迭代遍历过程中,其他多线程或者当前线程对该集合进行增加或者删除元素等操作,当前线程迭代器读取集合时会立马抛出一个ConcurrentModificationException异常,避免数据不一致。实现原理是迭代器在创建时,会获取集合的计数变量当作一个标记,迭代过程中,若发现该标记大小与计数变量不一致了,就以为集合做了新增或者删除等操作,就会抛出快速失败的异常。在ArrayList默认启用该机制。

2. Fail-Safe(安全失败)机制
安全失败机制是指集合在迭代遍历过程中,若其他多线程或者当前线程对该集合进行修改(增加、删除等元素)操作,当前线程迭代器仍然可以正常继续读取集合遍历,而不会抛出异常。该机制的实现,是通过迭代器在创建时,对集合进行了快照操作,即迭代器遍历的是原集合的数组快照副本,若在这个过程,集合进行修改操作,会将原有的数组内容复制到新数组上,并在新数组上进行修改,修改完成后,再将集合数组的引用指向新数组,,而读取操作仍然是访问旧的快照副本,故而实现读写分离,保证读取操作的线程安全性。在CopyOnWriteArrayList默认启用该机制。

基于这两个策略,分别写一个案例来说明。

一、迭代器的Fail-Fast(快速失败)机制原理

Fail-Fast(快速失败)机制案例,用集合ArrayList来说明,这里用一个线程就能模拟出该机制——

  public static void main(String[] args) {ArrayList<String> list = new ArrayList<>();list.add("张三");list.add("李四");list.add("王五");Iterator iterator = list.iterator();while(iterator.hasNext()) {//第一次遍历到这里,能正常打印,第二次遍历到这里,因上一次遍历做了list.add("李华")操作,集合已经改变,故而出现Fail-Fast(快速失败)异常String item = (String)iterator.next();list.add("李华");System.out.println(item);}System.out.println(list);}

执行这段代码,打印日志出现异常ConcurrentModificationException,说明在遍历过程当中,操作 list.add("李华")对集合做新增操作后,就会出现Fail-Fast(快速失败)机制,抛出异常,阻止继续进行遍历——

张三
Exception in thread "main" java.util.ConcurrentModificationExceptionat java.util.ArrayList$Itr.checkForComodification(ArrayList.java:911)at java.util.ArrayList$Itr.next(ArrayList.java:861)at ListExample.IteratorTest.main(IteratorTest.java:23)

这里面是怎么实现该Fail-Fast(快速失败)机制的呢?

先来看案例里创建迭代器的这行代码Iterator iterator = list.iterator(),底层是这样的——

 public Iterator<E> iterator() {return new Itr();}

Itr类是ArrayList内部类,实现了Iterator 接口,说明它本质是ArrayList内部一个迭代器。这里省略部分暂时无关紧要的代码,只需关注hasNext()和next()即可——

  private class Itr implements Iterator<E> {int cursor;       // 迭代计数器int lastRet = -1; // index of last element returned; -1 if no suchint expectedModCount = modCount;//判断是否已经迭代到最后一位public boolean hasNext() {return cursor != size;}//取出当前遍历到集合元素public E next() {//判断集合是否有做新增或者删除操作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];}......final void checkForComodification() {if (modCount != expectedModCount)throw new ConcurrentModificationException();}
}

再进入案例里的这行代码 String item = (String)iterator.next()底层,也就是Itr类的public E next() {......}方法。

注意next()里的这个方法 checkForComodification(),进入到方法里,可以看到,ConcurrentModificationException异常正是在这个方法里抛出来的,它做了一个判断,判断modCount是否等于expectedModCount,若不等于,就抛出快速失败异常。

  final void checkForComodification() {if (modCount != expectedModCount)throw new ConcurrentModificationException();}

那么,问题就简单了,研究ArrayList快速失败机制,本质只需要看modCount和expectedModCount是什么,就知道ArrayList的Fail-Fast(快速失败)机制是怎么处理的了。

在内部类Itr中,定义int expectedModCount = modCount,说明expectedModCount是在迭代器new Itr()创建时,就将此时的modCount数值赋值给变量expectedModCount,意味着,在整个迭代器生命周期内,这个expectedModCount是固定的了,从变量名就可以看出,它表示集合预期修改的次数,而modCount应该就是表示列表修改次数。假如迭代器创建时,modCount修改次数是5,那么整个迭代器生命周期内,预期的修改次数expectedModCount就只能等于5。

请注意最为关键的一个地方,modCount是可以变的。

先看一下在ArrayList里,这个modCount是什么?

这个modCount是定义在ArrayList的父类AbstractList里的——

/***这个列表在结构上被修改的次数。结构修改是指改变列表,或者以其他方式扰乱它,使其迭代进步可能产生不正确的结果。**该字段由迭代器和列表迭代器实现使用,由{@code迭代器}和{@code listtiterator}方法返回。*如果该字段的值发生了意外变化,迭代器(或列表)将返回该字段迭代器)将抛出{@code ConcurrentModificationException} *在响应{@code next}, {@code remove}, {@code previous},{@code set}或{@code add}操作。这提供了快速故障行为。**/
protected transient int modCount = 0;

根据注释,可以得知,这是一个专门记录列表被修改的次数,在ArrayList当中,涉及到add新增、remove删除、fastRemove、clear等涉及列表结构改动的操作,,都会通过modCount++形式,增加列表在结构上被修改的次数。

modCount表示列表被修改的次数。

我们在案例代码里,做了add操作——

while(iterator.hasNext()) {String item = (String)iterator.next();list.add("李华");System.out.println(item);
}

进入到ArrayList的add方法源码里,可以看到,在add新增过程中,按照ensureCapacityInternal =》ensureExplicitCapacity执行顺序,最后通过modCount++修改了变量modCount——

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++;// overflow-conscious codeif (minCapacity - elementData.length > 0)grow(minCapacity);
}

总结一下,迭代器创建时,变量expectedModCount是被modCount赋值,在整个迭代器等生命周期中,变量expectedModCount值是固定的了,但在第一轮遍历过程中,通过list.add("李华")操作,导致modCount++,最终就会出现expectedModCount != modCount。因此,在迭代器进行第二轮遍历时,执行到 String item = (String)iterator.next(),在next()里调用checkForComodification() 判断expectedModCount是否还等于modCount,这时已经不等于,故而就会抛出ConcurrentModificationException异常,立刻结束迭代器遍历,避免数据不一致。

 final void checkForComodification() {if (modCount != expectedModCount)throw new ConcurrentModificationException();}

以上,就是集合迭代器的Fail-Fast机制原理。

image

二、迭代器的Fail-Safe(安全失败)机制原理

Fail-Fast(快速失败)机制案例,用集合CopyOnWriteArrayList来说明,这里用一个线程就能模拟出该机制——

   public static void main(String[] args) {CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>();list.add("张三");list.add("李四");list.add("王五");Iterator iterator = list.iterator();while(iterator.hasNext()) {String item = (String)iterator.next();list.add("李华");System.out.println(item);}System.out.println("最后全部打印集合结果:" + list);}

执行这段代码,正常打印结果,说明在迭代器遍历过程中,对集合做了新增元素操作,并不影响迭代器遍历,新增的元素不会出现在迭代器遍历当中,但是,在迭代器遍历完成后,再一次打印集合,可以看到新增的元素已经在集合里了——

张三
李四
王五
最后全部打印集合结果:[张三, 李四, 王五, 李华, 李华, 李华]

Fail-Safe(安全失败)机制在CopyOnWriteArrayList体现,可以理解成,这是一种读写分离的机制。

下面就看一下CopyOnWriteArrayList是如何实现读写分离的。

先来看迭代器的创建Iterator iterator = list.iterator(),进入到list.iterator()底层源码——

public Iterator<E> iterator() {return new COWIterator<E>(getArray(), 0);
}

这里的COWIterator是一个迭代器,关键有一个地方,在创建迭代器对象,调用其构造器时传入两个参数,分别是getArray()和0。

这里的getArray()方法,获取到一个array数组,它是CopyOnWriteArrayList集合真正存储数据的地方。

final Object[] getArray() {return array;
}

另一个参数0,表示迭代器遍历的索引值,刚开始,肯定是从数组下标0开始。

明白getArray()和0这两个参数后,看一下迭代器创建new COWIterator(getArray(), 0)的情况,只需关注与本文有关的代码即可,其他暂时省略——

static final class COWIterator<E> implements ListIterator<E> {//列表快照private final Object[] snapshot;//调用next返回的元素的索引private int cursor;private COWIterator(Object[] elements, int initialCursor) {cursor = initialCursor;snapshot = elements;}public boolean hasNext() {return cursor < snapshot.length;}......@SuppressWarnings("unchecked")public E next() {if (! hasNext())throw new NoSuchElementException();return (E) snapshot[cursor++];}
}

在代码案例中,迭代器遍历过程时,通过hasNext()判断集合是否遍历完成,若还有没遍历的元素,就会调用 String item = (String)iterator.next()取出集合对应索引的元素。

从COWIterator类的next()方法中,可以看到,其元素是根据索引cursor从数组snapshot中取出来的。

这个snapshot就相当一个快照副本,在创建迭代器时,即new COWIterator(getArray(), 0),通过getArray()将此时CopyOnWriteArrayList集合的array数组引用复制给COWIterator的数组snapshot,那么snapshot引用和array引用都将指向同一个数组地址了。

只需保证snapshot指向的数组地址元素不变,那么整个迭代器读取集合数组就不会受影响。

image

如何做到snapshot指向的数组地址元素不变,但是又需要同时能满足CopyOnWriteArrayList集合的新增或者删除操作呢?

先来看一下CopyOnWriteArrayList的 list.add("李华")操作,具体实现能够在这块源码里看到,主要以下步骤:

1、add方法用到了ReentrantLock锁,在进行新增过程中,通过lock锁保证线程安全。

2、Object[] elements = getArray()这里的getArray()方法,和创建迭代器传的参数getArray()是同一个,都是获取到CopyOnWriteArrayList的array数组。取出array数组以及计算其长度后,创建一个比array数组长度大1的新数组,通过Arrays.copyOf(elements, len + 1)将array数组元素全部复制到新数组newElements。

3、在新数组newElements进行新增元素操作。

4、将CopyOnWriteArrayList的array数组引用指向新数组newElements,这样array=newElements,完成新增操作。

  public boolean add(E e) {final ReentrantLock lock = this.lock;lock.lock();try {//获取到CopyOnWriteArrayList的array数组Object[] elements = getArray();//获取array数组长度int len = elements.length;//将array数组数据,全部复制到一个长度比旧数组多1的新数组里Object[] newElements = Arrays.copyOf(elements, len + 1);//在新数组里,新增一个元素newElements[len] = e;//将CopyOnWriteArrayList的array数组引用指向新数组newElementssetArray(newElements);return true;} finally {lock.unlock();}}

可见,CopyOnWriteArrayList实现读写分离的原理,就是在COWIterator迭代器创建时,将此时的array数组指向的地址复制给snapshot,相当做了一次快照,迭代器遍历该快照数组地址元素。

后续涉及到列表修改相关的操作,会将原始array数组全部元素复制到一个新数组上,在新数组里面进行修改操作,这样就不会影响到迭代器遍历原来的数组地址里的数据了。(这也表明,这种读写分离只适合读多写少,在写多情况下,会出现性能问题)

新数组修改完毕后,只需将array数组引用指向新数组地址,就能完成修改操作了。

image

整个过程就能完成读写分离机制,即迭代器的Fail-Safe(安全失败)机制。

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

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

相关文章

009:vue结合el-table实现表格行拖拽排序(基于sortablejs)

文章目录 1. 实现效果2. 安装 sortablejs 插件3. 完整组件代码4. 注意点 1. 实现效果 2. 安装 sortablejs 插件 sortablejs 更多用法 cnpm i --save sortablejs3. 完整组件代码 <template><div class"home"><div class"body"><el-ta…

可以加速 Pandas(即使在 CPU 环境中)而无需编码...... FireDucks

引言 使用 Pandas 处理大量数据时&#xff0c;是否曾因处理时间过长而感到沮丧&#xff1f; 显然&#xff0c;一个库已经发布&#xff0c;可以在不改变现有代码的情况下加速 Pandas。 由 NEC Laboratories 发布的名为 FireDucks 的库的 Beta 版本可以免费使用。 而且&#xff…

浅谈WPF之控件模板Control Template和数据模板Data Template

WPF不仅支持传统的Windows Forms编程的用户界面和用户体验设计&#xff0c;同时还推出了以模板为核心的新一代设计理念。在WPF中&#xff0c;通过引入模板&#xff0c;将数据和算法的“内容”和“形式”进行解耦。模板主要分为两大类&#xff1a;数据模板【Data Template】和控…

linux cat命令增加-f显示文件名功能

在使用cat命令配合grep批量搜索文件内容时&#xff0c;我仅仅能知道是否搜索到&#xff0c;不知道是在哪个文件里找到的。比如cat ./src/*.c | grep full_write,在src目录下的所有.c文件里找full_write,能匹配到所有的full_write&#xff0c;但是不知道它们分别在哪些文件里。于…

【八】【C语言\动态规划】1567. 乘积为正数的最长子数组长度、413. 等差数列划分、978. 最长湍流子数组,三道题目深度解析

动态规划 动态规划就像是解决问题的一种策略&#xff0c;它可以帮助我们更高效地找到问题的解决方案。这个策略的核心思想就是将问题分解为一系列的小问题&#xff0c;并将每个小问题的解保存起来。这样&#xff0c;当我们需要解决原始问题的时候&#xff0c;我们就可以直接利…

黑客攻击服务器之后如何清除痕迹?如何进行伪装和逃脱追踪?

黑客攻击服务器之后如何清除痕迹?如何进行伪装和逃脱追踪?附完整执行代码。 在攻击结束后,如何不留痕迹的清除日志和操作记录,以掩盖入侵踪迹,这其实是一个细致的技术活。你所做的每一个操作,都要被抹掉;你所上传的工具,都应该被安全地删掉。 黑客的一次攻击行为,主…

HarmonyOS应用开发-仿微信UI实现

在本篇博客中&#xff0c;介绍一个仿微信的 HarmonyOS 应用&#xff0c;应用包括微信的首页、通讯录、发现、我的页面&#xff0c;以及聊天界面。 一、先上效果图&#xff1a; 二、代码解读 以聊天界面为例&#xff0c;代码如下&#xff08;解读在代码下面&#xff09;&#…

【华为数据之道学习笔记】7-3基于物理世界的“硬感知”能力

“硬感知”能力的分类 数据采集方式主要经历了人工采集和自动采集两个阶段。自动采集技术仍在发展中&#xff0c;不同的应用领域所使用的具体技术手段也不同。基于物理世界的“硬感知”依靠的就是数据采集&#xff0c;是将物理对象镜像到数字世界中的主要通道&#xff0c;是构建…

STM32F407-14.3.10-表73具有有断路功能的互补通道OCx和OCxN的输出控制位-1x000-1x111(总结)

基于表73中&#xff0c;主输出使能&#xff08;MOE1&#xff09;的8种OCx与OCxN的输出状态及波形图&#xff0c;已经单独整理输出8篇文章&#xff0c;方便需要时单独回查。 主输出使能时&#xff08;MOE1&#xff09;总结如下 通过表73中可得以下结论 1、控制位1x000与1x100…

ffmpeg两种windows版本区别说明

版本一 必须拷贝exe和dll文件才能使用&#xff0c;如果缺少dll则exe不正正常执行 如果缺少dll &#xff0c;执行 exe会报错如下 版本2 直接拷贝exe就能使用&#xff0c;没有依赖的环境

test perf-03-性能测试之 Gatling 使用入门官方教程

quick start 快速入门 学习 Gatling 的概念&#xff0c;使用录制器创建可运行的 Gatling 仿真。 介绍 在这一部分&#xff0c;我们将使用 Gatling 进行负载测试一个简单的云托管的 Web 服务器&#xff0c;并向您介绍 DSL&#xff08;领域特定语言&#xff09;的基本要素。 …

机器学习 -- 数据预处理

系列文章目录 未完待续…… 目录 系列文章目录 前言 一、数值分析简介 二、内容 前言 tips&#xff1a;这里只是总结&#xff0c;不是教程哈。 以下内容仅为暂定&#xff0c;因为我还没找到一个好的&#xff0c;让小白&#xff08;我自己&#xff09;也能容易理解&#x…

git 如何将某个分支的某个提交复制到另外一个分支

请直接去看原文: 原文链接:git 如何将某个分支的某个提交复制到另外一个分支_gitlab里面的markdown文件可以复用其他分支的吗-CSDN博客 --------------------------------------------------------------------------------------------------------------------------------…

防火墙什么用,软件防火墙与硬件防火墙有什么不一样

防火墙是一种网络安全技术&#xff0c;通过有机结合各类用于安全管理与筛选的软件和硬件设备&#xff0c;在计算机网络的内、外网之间构建一道相对隔绝的保护屏障&#xff0c;以保护用户资料与信息的安全性。 防火墙的作用的详细说明&#xff1a; 1.访问控制&#xff1a;防火…

SpingBoot的项目实战--模拟电商【2.登录】

&#x1f973;&#x1f973;Welcome Huihuis Code World ! !&#x1f973;&#x1f973; 接下来看看由辉辉所写的关于SpringBoot电商项目的相关操作吧 目录 &#x1f973;&#x1f973;Welcome Huihuis Code World ! !&#x1f973;&#x1f973; 一.功能需求 二.代码编写 …

如何使用mac电脑,1、使用快捷命令打开访达,2、使用终端命令创建文件,3、使用命令打开创建的文件,并且在vscode中打开

如何使用mac电脑 1、使用快捷命令打开访达 optioncommand空格键 快速进入访达 shiftcmmandn 创建一个空目录 2、使用终端命令创建文件 2.1进入文件夹 在终端页面输入“cd /Users/yunf/Desktop/”并按回车键&#xff08;此时进入到桌面文件夹&#xff0c;如果需要进入到其它…

算法分析-回溯算法-求解N皇后问题

一.题目需求 n皇后问题是一道比较经典的算法题。它研究的是将n个皇后放置在一个nn的棋盘上&#xff0c;使皇后彼此之间不相互攻击。 即任意两个皇后都不能处于同一行、同一列或同一斜线上。 二.算法思想 1.构建棋盘 可以用一个nn列表来表示棋盘&#xff0c;设皇后所在的位…

Vue 问题解决

一、问题&#xff1a;TypeError: (0 , _message.default) is not a function 当没有default时,在其他页面import引入的时&#xff0c;必须加{}。 二、问题&#xff1a;Vue前端页面的表格数据总是一行一行的显示 使用Async/Await来解决前端数据一行一行显示的问题。可以将获取部…

vue+element+springboot实现多张图片上传

1.需求说明 2.实现思路 3.el-upload组件主要属性说明 4.前端传递MultipartFile数组与服务端接收说明 5.完整代码 1.需求说明 动态模块新增添加动态功能,支持多张图片上传.实现过程中对el-upload组件不是很熟悉,踩了很多坑,当然也参考过别的文章,发现处理很…

Spark RDD操作性能优化技巧

Apache Spark是一个强大的分布式计算框架&#xff0c;用于处理大规模数据。然而&#xff0c;在处理大数据集时&#xff0c;性能优化成为一个关键问题。本文将介绍一些Spark RDD操作的性能优化技巧&#xff0c;帮助大家充分利用Spark的潜力&#xff0c;并获得更快的处理速度。 …