大白话之fail-fast | fail-safe:为什么会有这个机制?它有什么作用?

前言

阅读本篇文章,你需要了解下列知识:

  • 多线程的实现(看过来
  • Iterator的使用
  • ArrayList的使用和如何实现Iterator

为什么会有这个机制?

举个栗子

  1. 有一杯水、两个人(黄渤和红雷)
  2. 黄渤拿起了水杯,开始喝水
  3. 红雷到达案发现场,想走水杯喝水
  4. 黄渤很生气,并锤了红雷一顿

映射关系

将上面的栗子翻译一下:

  1. 有一个ArrayList、两个线程(Thread1Thread2
  2. Thread1请求并开始使用Iterator遍历ArrayList
  3. Thread2随后紧跟请求对ArrayList进行修改
  4. 由于Thread1正在遍历ArrayListArrayListThread2扔出ConcurrentModificationException

继承关系

Tip 如果下面的知识让你难以搞懂,可以直接跳过,在深入学习接口和继承后,再回来看一遍。

6.png

看起来有些困难?没关系。

ArrayList继承了Iterable接口,实现了基于Iterator的遍历功能。

ArrayList实现Iterator的部分源码如下:

/** * An optimized version of AbstractList.Itr */ private class Itr implements Iterator<E> { int cursor; // index of next element to return int lastRet = -1; // index of last element returned; -1 if no such int expectedModCount = modCount; // prevent creating a synthetic constructor Itr() {} public boolean hasNext() { return cursor != size; } @SuppressWarnings("unchecked") 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]; } public void remove() { if (lastRet < 0) throw new IllegalStateException(); checkForComodification(); try { ArrayList.this.remove(lastRet); cursor = lastRet; lastRet = -1; expectedModCount = modCount; } catch (IndexOutOfBoundsException ex) { throw new ConcurrentModificationException(); } } @Override public void forEachRemaining(Consumer<? super E> action) { Objects.requireNonNull(action); final int size = ArrayList.this.size; int i = cursor; if (i < size) { final Object[] es = elementData; if (i >= es.length) throw new ConcurrentModificationException(); for (; i < size && modCount == expectedModCount; i++) action.accept(elementAt(es, i)); // update once at end to reduce heap write traffic cursor = i; lastRet = i - 1; checkForComodification(); } } final void checkForComodification() { if (modCount != expectedModCount) throw new ConcurrentModificationException(); } }

throw new ConcurrentModificationException();Itr类中被赋予了四次条件执行:

方法字段触发条件
next()if (i >= elementData.length) throw new ConcurrentModificationException();当枚举的指针大于等于ArrayList的长度时,抛出错误
remove()try {ArrayList.this.remove(lastRet); cursor = lastRet; lastRet = -1; expectedModCount = modCount; } catch (IndexOutOfBoundsException ex) {throw new ConcurrentModificationException(); }当删除ArrayList中指定对象时出错(对象不存在),抛出错误
forEachRemaining()if (i >= es.length) throw new ConcurrentModificationException();当枚举的指针大于等于ArrayList的长度时,抛出错误
checkForComodification()if (modCount != expectedModCount) throw new ConcurrentModificationException();检测当运行中统计对象的数量不等于预计中对象的数量时,抛出错误

它有什么作用?

当多个线程同时对一个对象进行高频率的增删改查时,可能会出现数据异常。

实例

例子很简单,我们创建两个线程,Thread1访问修改ArrayListThread2Thread1尚未访问完毕时同时对ArrayList进行访问修改:

打开你的IDE,新建类Main.java并复制下方代码:

import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Date; import java.util.Iterator; import java.util.List; public class Main { //需要操作的ArrayList private static List arrayList = new ArrayList<>(); public static void main(String[] args) { //将类Thr实例化为两个线程 Thread thread1 = new Thr(); Thread thread2 = new Thr(); //同时启动两个线程进行操作 thread1.start(); thread2.start(); } private static void printAll(String threadName) { System.out.println("由线程" + threadName + "遍历后,当前ArrayList的内容:"); //获取ArrayList的Iterator枚举器 Iterator iterator = arrayList.iterator(); //将ArrayList中全部内容遍历并打印 while(iterator.hasNext()) { System.out.print(iterator.next()); } System.out.println(); } static class Thr extends Thread { public void run() { System.out.println("线程" + Thread.currentThread().getName() + "开始运行!当前时间" + new SimpleDateFormat("mm:ss:SS").format(new Date())); //将0-5写入到ArrayList中,每次写入打印一次ArrayList中全部的内容 for (int i = 0; i < 5; i++) { //将i写入到ArrayList arrayList.add(i); //打印ArrayList中全部的内容 printAll(Thread.currentThread().getName()); } } } }

运行结果:

线程Thread-0开始运行!当前时间14:04:358 线程Thread-1开始运行!当前时间14:04:359 由线程Thread-1遍历后,当前ArrayList的内容: 0由线程Thread-0遍历后,当前ArrayList的内容: 00 由线程Thread-0遍历后,当前ArrayList的内容: 001 由线程Thread-0遍历后,当前ArrayList的内容: 0012 由线程Thread-0遍历后,当前ArrayList的内容: 0Exception in thread "Thread-1" 00123 由线程Thread-0遍历后,当前ArrayList的内容: 0java.util.ConcurrentModificationException 01234 at java.base/java.util.ArrayList$Itr.checkForComodification(ArrayList.java:1042) at java.base/java.util.ArrayList$Itr.next(ArrayList.java:996) at failfast.Main.printAll(Main.java:28) at failfast.Main$Thr.run(Main.java:41)

请注意 你的运行结果可能和我的运行结果会有以下两种偏差:

  1. 两个线程的启动顺序、启动时间
  2. 可能不会产生报错(由于两个线程读取ArrayList的时间恰巧错开)

你可能发现了一个规律:

结果是随机的,最重要的是ConcurrentModificationException是不稳定的。即使两个线程同时访问,也有一部分可能性不会抛出ConcurrentModificationException。

为什么呢?让我们看看官方对fail-fast机制的解释:

下方解释部分来源于https://www.cnblogs.com/kubidemanong/articles/9113820.html

迭代器的fail-fast行为是不一定能够得到保证的。一般来说,存在非同步的并发修改时,是不能够保证错误一定被抛出的。但是会做出最大的努力来抛出ConcurrentModificationException

因此,编写依赖于此异常的程序的做法是不正确的。正确的做法应该是:迭代器的fail-fast行为应该仅用于检测程序中的Bug。

避免fail-fast

ArrayList使用fail-fast机制自然是因为它增强了数据的安全性。但在某些场景,我们可能想避免fail-fast机制产生的错误,这时我们就要将ArrayList替换为使用fail-safe机制的CopyOnWriteArrayList

将:

private static List arrayList = new ArrayList<>();

修改为:

private static List arrayList = new CopyOnWriteArrayList<>();

CopyOnWriteArrayListIterator的实现上没有设计抛出ConcurrentModificationException的代码段,所以便避免了fail-fast机制错误的抛出。我们将它称之为fail-safe机制

后语

本篇篇幅较长,涉及的知识点较乱。如果你有问题,可以在下方评论中提出;如果你发现本篇文章中存在误导内容,也请在评论中及时告知,谢谢!

如转载请在文章尾部添加

原作者来自 adlered 个人技术博客:https://www.stackoverflow.wiki/

评论

取消