前言
阅读本篇文章,你需要了解下列知识:
- 多线程的实现(看过来)
- Iterator 的使用
- ArrayList 的使用和如何实现 Iterator
为什么会有这个机制?
举个栗子
- 有一杯水、两个人(黄渤和红雷)
- 黄渤拿起了水杯,开始喝水
- 红雷到达案发现场,想抢走水杯喝水
- 黄渤很生气,并锤了红雷一顿
映射关系
将上面的栗子翻译一下:
- 有一个
ArrayList
、两个线程(Thread1
和Thread2
) Thread1
请求并开始使用Iterator
遍历ArrayList
Thread2
随后紧跟请求对ArrayList
进行修改- 由于
Thread1
正在遍历ArrayList
,ArrayList
对Thread2
扔出ConcurrentModificationException
继承关系
Tip 如果下面的知识让你难以搞懂,可以直接跳过,在深入学习接口和继承后,再回来看一遍。
看起来有些困难?没关系。
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
访问修改 ArrayList
,Thread2
在 Thread1
尚未访问完毕时同时对 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)
请注意 你的运行结果可能和我的运行结果会有以下两种偏差:
- 两个线程的启动顺序、启动时间
- 可能不会产生报错(由于两个线程读取 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<>();
CopyOnWriteArrayList
在 Iterator
的实现上没有设计抛出 ConcurrentModificationException 的代码段,所以便避免了 fail-fast机制
错误的抛出。我们将它称之为 fail-safe机制
。
后语
本篇篇幅较长,涉及的知识点较乱。如果你有问题,可以在下方评论中提出;如果你发现本篇文章中存在误导内容,也请在评论中及时告知,谢谢!
欢迎来到这里!
我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。
注册 关于