【JavaSE】fail-fast与fail-safe源码分析

news/2024/11/25 0:57:01/

文章目录

  • 1. fail-fast与fail-safe概述
  • 2. fail-fast源码分析
  • 3. fail-safe源码分析
  • 4. 总结

1. fail-fast与fail-safe概述

快速失败(fail-fast),快速失败是Java集合的一种错误检测机制。

  1. 出现场景:线程A在使用迭代器遍历一个集合对象的时候,线程B对集合对象的内存进行了操作(增加、删除、修改),这时候会抛出Concurrent Modification Exception
  2. 原理:就拿ArrayList来说,ArrayList继承了一个抽象类AbstractList,这个抽象了中有一个成员变量protected transient int modCount = 0;,这个变量是记录集合被修改的次数的。集合使用迭代器进行遍历的时候,每当迭代器使用hashNext()/next()之前,会先检测modCount变量是否为expectedmodCount值,是的话就遍历;否则抛出异常。
  3. 注意:这里抛出异常的判断条件为modCount变量是否为expectedmodCount值,如果集合发生变化时,modCount值刚好又设置为了expectedmodCount值。因此,不能依赖于这个异常是否抛出而进行并发操作的编程,这个异常只建议用于检测并发修改的bug。
  4. 场景:java.util包下的集合类都是快速失败的,不能在多线程下发生并发修改(迭代过程中被修改),比如ArrayList 类。

安全失败(fail—safe),采用安全失败的集合容器,在遍历时不是直接在集合内容上访问的,而是先复制原有集合内容,在拷贝的集合上进行遍历。

  1. 原理:由于迭代时是对原集合的拷贝进行遍历,所以在遍历过程中对原集合所作的修改并不能被迭代器检测到,所以不会触发Concurrent Modification Exception
  2. 缺点:基于拷贝内容的优点是避免了Concurrent Modification Exception,但同样地,迭代器并不能访问到修改后的内容,也就是说迭代器遍历的是开始遍历那一刻拿到的集合拷贝,在遍历期间原集合发生的修改迭代器是不知道的。
  3. 场景java.util.concurrent包下的容器都是安全失败,可以在多线程下并发使用,并发修改,比如CopyOnWriteArrayList类。

2. fail-fast源码分析

测试代码,使用IDEA进行debug

public static void main(String[] args) {ArrayList<Integer> list = new ArrayList<>();list.add(1);list.add(2);list.add(3);list.add(4);for (Integer integer : list) {System.out.println(integer);}
}

这里的增强for循环实际上就是使用迭代器进行迭代的。迭代器记录了当前集合的修改次数。

int expectedModCount = modCount;

在for循环之前,list集合中添加了四个元素,所以在list中的modCount的值为4。

使用IDEA的debug工具,在程序执行过程中往list集合中添加一个元素——5,模拟并发。

image-20230201234022290

image-20230201234042067

成功往集合中添加第五个元素。

这时候,当下一次增强for循环执行的时候,也就是使用迭代器的hasNext方法

public boolean hasNext() {return cursor != size;
}

然后执行迭代器的next方法

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];
}

首先,会调用checkForComodification(),检验集合是否被修改,如果修改,那么集合中的mod不会与expectedModCount相等。

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

modexpectedModCount不相等,则会抛出ConcurrentModificationException异常,程序中断。

image-20230201234738430


3. fail-safe源码分析

使用CopyOnWriteArrayList进行测试。

public static void main(String[] args) {CopyOnWriteArrayList<Integer> list = new CopyOnWriteArrayList<Integer>();list.add(1);list.add(2);list.add(3);list.add(4);for (Integer integer : list) {System.out.println(integer);}
}

先来简单阅读一下CopyOnWriteArrayList的源码

//底层存放数据的数组
private transient volatile Object[] array;
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();}
}final Object[] getArray() {return array;
}final void setArray(Object[] a) {array = a;
}

add方法,它底层实现是加了ReentrantLock锁。

首先获取底层数组的长度。然后进行扩容。接着将需要添加进集合的元素放置在扩容后的数组的末端。

再将新数组newElements赋值给底层数组array

接下来在增强for循环打断点。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qrm6VV5A-1675313411939)(https://xingqiu-tuchuang-1256524210.cos.ap-shanghai.myqcloud.com/9944/image-20230202123227242.png)]

在准备开始增强for循环的时候,会调用iterator创建一个COWIterator迭代器。

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

并且将底层存放数据的数组array作为参数。

//由后续调用next返回的元素的索引。
private int cursor;
//快照,存储的是迭代器进行迭代的时候,集合的数据
private final Object[] snapshot;
private COWIterator(Object[] elements, int initialCursor) {cursor = initialCursor;snapshot = elements;
}

然后调用hasNext方法

public boolean hasNext() {return cursor < snapshot.length;
}

接着,调用next方法,将快照的数组依次输出。

public E next() {if (! hasNext())throw new NoSuchElementException();return (E) snapshot[cursor++];
}

假如这时候,线程B向集合中添加一个元素。但是迭代器中的snapshot并没有变化。

因为能snapshotCOWIterator初始化的时候已经固定了。即使接下来array发生改变。snapshot也依然不变。

所以迭代器输出的数据是迭代器创建那一刻之前的数据,迭代过程中无论集合原数据变成什么,都是输出旧数据。


4. 总结

  1. ArrayListfail-fast的经典代表,遍历的同时不能修改,否则会抛出异常。
  2. CopyOnWriteArrayListfail-safe的经典代表,遍历的同时可以修改,原理是读写分离。


http://www.ppmy.cn/news/22831.html

相关文章

《计算机构造与解释》读书笔记(5)

文章目录1. 写在最前面2. 流2.1 流作为延时的表2.1.1 流实现的行为方式2.1.2 delay 和 force 的实现2.2 无穷流2.2.1 隐式地定义流2.3 流计算模式的使用2.3.1 系统地将迭代操作方式表示为流过程2.3.2 序对的无穷流2.3.3 将流作为信号2.4 流和延时求值2.4.1 规范求值序2.5 函数式…

java基于springboot+vue的企业员工工资考勤系统

随着我国改革开发和国家政策的开发等一系列优惠条件的开放&#xff0c;我国的高校数量也是在不断的增加&#xff0c;每个高校都有很多的员工&#xff0c;每个员工的工资又各不相同&#xff0c;如何能够管理这些庞大的工资数据&#xff0c;是很多高校在发放工资的时候都需要面临…

介绍JVM中OOM的8种类型

title: 介绍JVM中OOM的8种类型 date: 2019-11-17 16:00:00 tags: JVM OOMOutOfMemoryError categories:JVM java.lang.OutOfMemoryError Java heap space Java应用程序仅允许使用有限的内存&#xff0c;此限制是在应用程序启动期间指定的。Java内存被分为两个不同的区域&…

在CentOS 7.7 x86_64上从python3安装pip3

线上发现一台服务器只安装python 3.7.3&#xff0c;但是没有安装pip3&#xff0c;这样就无法安装Python的各种包&#xff0c;分析是从二进制包安装的&#xff0c;并不完整。如果从python的源码包安装的话&#xff0c;默认是包含pip3的。 问题&#xff1a; 有python 3.7但是没…

第一章 Arm 架构科普解读

本文源自《书香度年华》「ARM 架构专栏」&#xff0c;是一系列由浅入深、循序渐进的文章&#xff0c;文章之间有一定的前后关联性&#xff0c;所以按顺序阅读&#xff0c;建议收藏专栏。 目录 前言 一、架构概述 1.1 冯诺依曼架构 1.2 哈佛架构 1.3 Arm 架构 二、架构图谱…

第07讲:Vue初级

一、浅试双向绑定 1.1、导入vue的js库 <script src"assets/vue.min-v2.5.16.js"></script>1.2、创建js代码段写vue代码 <script>//创建vue实例new Vue({el: #app, //将id为app的div的管理权交给vuedata: { //用户共享的数据msg: null}});</s…

SEO搜索引擎优化方式

SEO搜索引擎优化方式 SEO搜索引擎优化方式 文章目录SEO搜索引擎优化方式[TOC](文章目录)前言一、黑帽SEO1、关键字的堆叠2、隐藏文本3、门页二、白帽SEO1、 TDK2、 提高网站语义化的html标签占比3、 SSR总结前言 利用搜索引擎的规则提高网站在有关搜索引擎内的自然排名&#…

Apache Flink 实现原理:容错机制

目录 前言 一、Flink容错 二、状态(State) 三、检查点(Checkpoint) 四、保存点(Savepoint)