前言
阅读本篇文章,你需要了解下列知识:
- 多线程的实现(看过来)
- 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机制
。
后语
本篇篇幅较长,涉及的知识点较乱。如果你有问题,可以在下方评论中提出;如果你发现本篇文章中存在误导内容,也请在评论中及时告知,谢谢!
欢迎来到这里!
我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。
注册 关于