概述
ConcurrentModificationException——并发修改异常
- 当你觉得自己玩转ArrayList,HashSet,HashMap的时候,一个可怕的异常会突然出现在你的面前:ConcurrentModificationException
- 不要被它的名字迷惑(并发修改异常),在单线程中,它也依旧会经常出现!
- 它长这个样子:
单线程ConcurrentModificationException场景
▶ for-each遍历场景
List<String> list = new ArrayList<>();
list.add("Hana");
list.add("Alice");
list.add("Cocoa");
for(String s : list){
if(s.equals("Alice")){
list.remove("Alice");
}
}
Set<String> set = new HashSet<>();
set.add("Hana");
set.add("Alice");
set.add("Cocoa");
for(String s : set){
if(s.equals("Alice")){
list.remove("Alice");
}
}
Map<String, String> map = new HashMap<>();
map.put("a", "Hana");
map.put("b", "Alice");
map.put("c", "Cocoa");
for(String key : map.keySet()){
if (map.get(key).equals("Alice")) {
map.remove(key);
}
}
>>> remove(..)换成add(..)同样报异常
▶ Iterator迭代场景
Iterator<String> it = list.iterator();
while (it.hasNext()){
String s = it.next();
if(s.equals("Alice")){
list.add("Alice");
}
}
Iterator<String> it = set.iterator();
while (it.hasNext()){
String s = it.next();
if(s.equals("Alice")){
set.add("Alice");
}
}
// HashMap迭代的实际上是key(即keySet()),就不再贴代码了
雾里看花,三句话假装看懂ConcurrentModificationException异常
- (★)
- 总结一下上面的场景——在对ArrayList、HashSet、HashMap进行迭代时(for-each的本质其实就是Iterator迭代器),也就是在循环时进行了增、删等操作
- 原因在于,迭代器有一个ModCount(修改计数),以及一个expectedModCount(期望的修改计数)
- 在一个迭代器初始的时,会被赋予容器的modCount,如果在迭代器遍历的过程中,没有保管好它,即二者不符(modCount != expectedModCount),抛ConcurrentModificationException异常
抽丝剥茧,剖开源码解析 ConcurrentModificationException异常
我们从头开始慢慢来。
首先刚刚说过,不管是for-each还是Iterator,归根结底都是使用了Iterator迭代器。我们突然反应过来,报错信息里有Itr
的字眼!不妨看看这个迭代器从何而来?
Iterator<String> it = list.iterator();
嫌疑剑指ArrayList的iterator()
方法。在它的抽象父类中,我们找到了该方法的源码(ArrayList源码中也有,但AbstractList中的更加简洁明了):
public Iterator<E> iterator() {
return new Itr();
}
它的下面,紧跟着Itr这个成员内部类的具体实现,这是它的全部源码:
private class Itr implements Iterator<E> {
/**
* Index of element to be returned by subsequent call to next.
*/
int cursor = 0;
/**
* Index of element returned by most recent call to next or
* previous. Reset to -1 if this element is deleted by a call
* to remove.
*/
int lastRet = -1;
/**
* The modCount value that the iterator believes that the backing
* List should have. If this expectation is violated, the iterator
* has detected concurrent modification.
*/
int expectedModCount = modCount;
// hasNext()的逻辑非常简单————当cursion没指到末尾(size()),就返回true,告诉迭代器可以放心去读下一个元素
public boolean hasNext() {
return cursor != size();
}
// next()的逻辑在介绍cursion和lastRet时已经详细地说了
// 值得万分注意的是,next的第一步,做了一次奇怪的检查————checkForComodification()
public E next() {
checkForComodification();
try {
int i = cursor;
E next = get(i);
lastRet = i;
cursor = i + 1;
return next;
} catch (IndexOutOfBoundsException e) {
checkForComodification();
throw new NoSuchElementException();
}
}
// 此remove非彼remove,不是真正用于删除元素的remove
public void remove() {
if (lastRet < 0)
throw new IllegalStateException();
checkForComodification();
try {
AbstractList.this.remove(lastRet);
if (lastRet < cursor)
cursor--;
lastRet = -1;
expectedModCount = modCount;
} catch (IndexOutOfBoundsException e) {
throw new ConcurrentModificationException();
}
}
// 恭喜你!!!这里就是ConcurrentModificationExceptio的万恶之源
// 这个检查的逻辑是,如果实际的修改次数modCount和期望的修改次数expectedModCount不符,便会抛出ConcurrentModificationExceptio异常
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
}
Itr类的四个成员变量立即引起了我们的注意:
-
cursor:表示下一个将要访问的元素的索引(读源码可知,每次调用next(),便会根据当前的cursion取出一个元素,然后+1,指向下一个元素)
-
lastRet:表示上一个访问的元素的索引(读源码可知,每次调用next(),在cursion+1之前,其值会先赋给lastRet——这样以来,cursion总是指向下一个要访问元素,lastRet总是落后一个,指向根据cursion刚刚访问过的元素。一个细节是,如果通过调用remove删除该元素,则lastRet重置为-1)
-
expectedModCount:表示对修改(增,删…)次数的期望值,它的初始值为modCount(在迭代器初始化的时候,容器的modCount被赋给它)
-
modCount:是AbstractList类中的一个成员变量,表示实际发生的修改词数,初始值为0
-
另外,hasNext()、next()、checkForComodification()更是关键!!!我给加上注释了,一定要读!!! (★)
理解了这四个值和这几个方法,我们马上将剖开真相!!!在ArrayList源码中搜索remove方法(返回值类型为Boolean的那个,别认错人了啊 >_<)
public boolean remove(Object o) {
if (o == null) {
for (int index = 0; index < size; index++)
if (elementData[index] == null) {
fastRemove(index);
return true;
}
} else {
for (int index = 0; index < size; index++)
if (o.equals(elementData[index])) {
fastRemove(index);
return true;
}
}
return false;
}
/*
* Private remove method that skips bounds checking and does not
* return the value removed.
*/
private void fastRemove(int index) {
modCount++;
int numMoved = size - index - 1;
if (numMoved > 0)
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
elementData[--size] = null; // clear to let GC do its work
}
remove()方法平平无奇,无非是根据传入的元素去遍历,试图去找相同的元素。如果找到,又调用了一个fastRemove()方法…重点来了!这里有一句modCount++;
,意思是说进行了一次修改——这便是ConcurrentModificationException异常的根源:
- 执行
list.remove("Alice")
时,remove(…)方法被调用,接着fastRemove(…)方法被调用 - fastRemove中执行了一句
modCount++;
———这毕竟是一次“修改操作(删除)” - 至此,还没有任何异常
- 接着,Iterator继续迭代。hasNext()方法返回true后,放心地去执行next()方法
- next方法的第一句就是进行了一个checkForComodification()检查,可此时modCount = 1, expectedModCount = 0
- modCount != expectedModCount,抛出ConcurrentModificationException异常
至此,真相大白。
再次体会一下上面总结的那三句话,是不是恍然大悟的呢 >_< ?
解决ConcurrentModificationException并发修改异常
▶ 方法一:将增删的操作移到迭代(循环)之外
for(String s : set){
if(s.equals("Alice")){
trash.add("Alice"); // 一会儿再删~
wait.add("Alice"); // 一会儿再加~
}
}
set.removeAll(trash); // 删
set.addAll(wait); // 加
▶ 方法二:使用线程安全的容器
线程不安全 | 线程安全 |
---|---|
ArrayList | CopyOnWriteArrayList |
HashSet | CopyOnWriteArraySet |
HashMap | ConcurrentHashMap |
List<String> list = new CopyOnWriteArrayList<>();
Set<String> set = new CopyOnWriteArraySet<>();
Map<String, String> map = new ConcurrentHashMap<>();
▶ 两种方法的评价
方法一是一种「慵懒」的编程思想,其实还是比较优雅的。但是对于多线程出现的ConcurrentModificationException异常无能为力
方法二直接使用线程安全的容器,通过加锁,解决一切问题(性能就是另一回事了)
举一反三,帮助理解
List<String> list = new ArrayList<>();
list.add("a");
list.add("b");
list.add("c");
list.add("d");
for(String s : list){
if(s.equals("a")){
list.remove("a");
}
}
此次next | remove(fastRemove) | hasNext判断 | 下一次next | |
---|---|---|---|---|
remove("a"); | cursion + 1 = 1 | size - 1 = 3,modCount++ | cursion != size,返回true | 检查,报错 |
remove("b"); | cursion + 1 = 2 | size - 1 = 3,modCount++ | cursion != size,返回true | 检查,报错 |
remove("a"); | cursion + 1 = 3 | size - 1 = 3,modCount++ | cursion == size,返回false | 不执行,不检查,不报错 |
remove("a"); | cursion + 1 = 4 | size - 1 = 3,modCount++ | cursion != size,返回true | 检查,报错 |
这里出现了一次巧妙的“避开”,如果现在还是不好理解,可以再看看这篇文章《ArrayList迭代时remove元素 , 未抛出ConcurrentModificationException异常》
完
最后
以上就是俭朴硬币为你收集整理的【Java】ConcurrentModificationException异常的源码深入分析与成功解除的全部内容,希望文章能够帮你解决【Java】ConcurrentModificationException异常的源码深入分析与成功解除所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复