前言
阅读本篇文章,你需要了解下列知识:
- 多线程的实现(看过来)
- Iterator的使用
- ArrayList的使用和如何实现Iterator
为什么会有这个机制?
举个栗子
- 有一杯水、两个人(黄渤和红雷)
- 黄渤拿起了水杯,开始喝水
- 红雷到达案发现场,想抢走水杯喝水
- 黄渤很生气,并锤了红雷一顿
映射关系
将上面的栗子翻译一下:
- 有一个
ArrayList
、两个线程(Thread1
和Thread2
)
Thread1
请求并开始使用Iterator
遍历ArrayList
Thread2
随后紧跟请求对ArrayList
进行修改
- 由于
Thread1
正在遍历ArrayList
,ArrayList
对Thread2
扔出ConcurrentModificationException
继承关系
Tip 如果下面的知识让你难以搞懂,可以直接跳过,在深入学习接口和继承后,再回来看一遍。
看起来有些困难?没关系。
1ArrayList继承了Iterable接口,实现了基于Iterator的遍历功能。
ArrayList实现Iterator的部分源码如下:
1 /**
2 * An optimized version of AbstractList.Itr
3 */
4 private class Itr implements Iterator<E> {
5 int cursor; // index of next element to return
6 int lastRet = -1; // index of last element returned; -1 if no such
7 int expectedModCount = modCount;
8
9 // prevent creating a synthetic constructor
10 Itr() {}
11
12 public boolean hasNext() {
13 return cursor != size;
14 }
15
16 @SuppressWarnings("unchecked")
17 public E next() {
18 checkForComodification();
19 int i = cursor;
20 if (i >= size)
21 throw new NoSuchElementException();
22 Object[] elementData = ArrayList.this.elementData;
23 if (i >= elementData.length)
24 throw new ConcurrentModificationException();
25 cursor = i + 1;
26 return (E) elementData[lastRet = i];
27 }
28
29 public void remove() {
30 if (lastRet < 0)
31 throw new IllegalStateException();
32 checkForComodification();
33
34 try {
35 ArrayList.this.remove(lastRet);
36 cursor = lastRet;
37 lastRet = -1;
38 expectedModCount = modCount;
39 } catch (IndexOutOfBoundsException ex) {
40 throw new ConcurrentModificationException();
41 }
42 }
43
44 @Override
45 public void forEachRemaining(Consumer<? super E> action) {
46 Objects.requireNonNull(action);
47 final int size = ArrayList.this.size;
48 int i = cursor;
49 if (i < size) {
50 final Object[] es = elementData;
51 if (i >= es.length)
52 throw new ConcurrentModificationException();
53 for (; i < size && modCount == expectedModCount; i++)
54 action.accept(elementAt(es, i));
55 // update once at end to reduce heap write traffic
56 cursor = i;
57 lastRet = i - 1;
58 checkForComodification();
59 }
60 }
61
62 final void checkForComodification() {
63 if (modCount != expectedModCount)
64 throw new ConcurrentModificationException();
65 }
66 }
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
并复制下方代码:
1import java.text.SimpleDateFormat;
2import java.util.ArrayList;
3import java.util.Date;
4import java.util.Iterator;
5import java.util.List;
6
7public class Main {
8 //需要操作的ArrayList
9 private static List arrayList = new ArrayList<>();
10
11 public static void main(String[] args) {
12 //将类Thr实例化为两个线程
13 Thread thread1 = new Thr();
14 Thread thread2 = new Thr();
15 //同时启动两个线程进行操作
16 thread1.start();
17 thread2.start();
18 }
19
20 private static void printAll(String threadName) {
21 System.out.println("由线程" + threadName + "遍历后,当前ArrayList的内容:");
22 //获取ArrayList的Iterator枚举器
23 Iterator iterator = arrayList.iterator();
24 //将ArrayList中全部内容遍历并打印
25 while(iterator.hasNext()) {
26 System.out.print(iterator.next());
27 }
28 System.out.println();
29 }
30
31 static class Thr extends Thread {
32 public void run() {
33 System.out.println("线程" + Thread.currentThread().getName() + "开始运行!当前时间" + new SimpleDateFormat("mm🇸🇸SS").format(new Date()));
34 //将0-5写入到ArrayList中,每次写入打印一次ArrayList中全部的内容
35 for (int i = 0; i < 5; i++) {
36 //将i写入到ArrayList
37 arrayList.add(i);
38 //打印ArrayList中全部的内容
39 printAll(Thread.currentThread().getName());
40 }
41 }
42 }
43
44}
运行结果:
1线程Thread-0开始运行!当前时间14:04:358
2线程Thread-1开始运行!当前时间14:04:359
3由线程Thread-1遍历后,当前ArrayList的内容:
40由线程Thread-0遍历后,当前ArrayList的内容:
500
6由线程Thread-0遍历后,当前ArrayList的内容:
7001
8由线程Thread-0遍历后,当前ArrayList的内容:
90012
10由线程Thread-0遍历后,当前ArrayList的内容:
110Exception in thread "Thread-1" 00123
12由线程Thread-0遍历后,当前ArrayList的内容:
130java.util.ConcurrentModificationException
1401234
15 at java.base/java.util.ArrayList$Itr.checkForComodification(ArrayList.java:1042)
16 at java.base/java.util.ArrayList$Itr.next(ArrayList.java:996)
17 at failfast.Main.printAll(Main.java:28)
18 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
:
将:
1private static List arrayList = new ArrayList<>();
修改为:
1private static List arrayList = new CopyOnWriteArrayList<>();
CopyOnWriteArrayList
在Iterator
的实现上没有设计抛出ConcurrentModificationException的代码段,所以便避免了fail-fast机制
错误的抛出。我们将它称之为fail-safe机制
。
后语
本篇篇幅较长,涉及的知识点较乱。如果你有问题,可以在下方评论中提出;如果你发现本篇文章中存在误导内容,也请在评论中及时告知,谢谢!