我是靠谱客的博主 俭朴硬币,最近开发中收集的这篇文章主要介绍【Java】ConcurrentModificationException异常的源码深入分析与成功解除,觉得挺不错的,现在分享给大家,希望可以做个参考。

概述

 

ConcurrentModificationException——并发修改异常

  • 当你觉得自己玩转ArrayListHashSetHashMap的时候,一个可怕的异常会突然出现在你的面前: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异常的根源:

 

  1. 执行list.remove("Alice")时,remove(…)方法被调用,接着fastRemove(…)方法被调用
  2. fastRemove中执行了一句 modCount++;———这毕竟是一次“修改操作(删除)”
  3. 至此,还没有任何异常
  4. 接着,Iterator继续迭代hasNext()方法返回true后,放心地去执行next()方法
  5. next方法的第一句就是进行了一个checkForComodification()检查,可此时modCount = 1, expectedModCount = 0
  6. modCount != expectedModCount,抛出ConcurrentModificationException异常

 

至此,真相大白。

再次体会一下上面总结的那三句话,是不是恍然大悟的呢 >_< ?

 
 
 

解决ConcurrentModificationException并发修改异常

方法一:将增删的操作移到迭代(循环)之外

for(String s : set){
    if(s.equals("Alice")){
        trash.add("Alice");		// 一会儿再删~
        wait.add("Alice");		// 一会儿再加~
    }
}
set.removeAll(trash);	// 删
set.addAll(wait);		// 加

方法二:使用线程安全的容器

线程不安全线程安全
ArrayListCopyOnWriteArrayList
HashSetCopyOnWriteArraySet
HashMapConcurrentHashMap
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");
	}
}
此次nextremove(fastRemove)hasNext判断下一次next
remove("a");cursion + 1 = 1size - 1 = 3,modCount++cursion != size,返回true检查,报错
remove("b");cursion + 1 = 2size - 1 = 3,modCount++cursion != size,返回true检查,报错
remove("a");cursion + 1 = 3size - 1 = 3,modCount++cursion == size,返回false不执行,不检查,不报错
remove("a");cursion + 1 = 4size - 1 = 3,modCount++cursion != size,返回true检查,报错

这里出现了一次巧妙的“避开”,如果现在还是不好理解,可以再看看这篇文章《ArrayList迭代时remove元素 , 未抛出ConcurrentModificationException异常

 
 
 
 
 
 
 
 
 
 
 
 
 
 

 
 

最后

以上就是俭朴硬币为你收集整理的【Java】ConcurrentModificationException异常的源码深入分析与成功解除的全部内容,希望文章能够帮你解决【Java】ConcurrentModificationException异常的源码深入分析与成功解除所遇到的程序开发问题。

如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。

本图文内容来源于网友提供,作为学习参考使用,或来自网络收集整理,版权属于原作者所有。
点赞(58)

评论列表共有 0 条评论

立即
投稿
返回
顶部