java.util.ConcurrentModificationException详解

本想翻译一下java.util.ConcurrentModificationException这篇文章的。但发现讲的不够详细深入,查了一些资料后决定自己扩展一下。水平有限,仅仅作为一个学习总结啦。

异常产生

当我们迭代一个ArrayList或者HashMap时,如果尝试对集合做一些修改操作(例如删除元素),可能会抛出java.util.ConcurrentModificationException的异常。

import java.util.Iterator;
import java.util.List;

public class AddRemoveListElement {
 
    public static void main(String args[]) {
        List<String> list = new ArrayList<String>();
        list.add("A");
        list.add("B");
 
        for (String s : list) {
            if (s.equals("B")) {
                list.remove(s);
            }
        }
        
        //foreach循环等效于迭代器
        /*Iterator<String> iterator=list.iterator();
        while(iterator.hasNext()){
            String s=iterator.next();
            if (s.equals("B")) {
                list.remove(s);
            }
        }*/    
    }
}

Output:


异常原因

ArrayList的父类AbstarctList中有一个域modCount,每次对集合进行修改(增添元素,删除元素……)时都会modCount++

而foreach的背后实现原理其实就是Iterator(关于Iterator可以看Java Design Pattern: Iterator),等同于注释部分代码。在这里,迭代ArrayList的Iterator中有一个变量expectedModCount,该变量会初始化和modCount相等,但如果接下来如果集合进行修改modCount改变,就会造成expectedModCount!=modCount,此时就会抛出java.util.ConcurrentModificationException异常

过程如下图:


绘图2.jpg

我们再来根据源码详细的走一遍这个过程

/*
 *AbstarctList的内部类,用于迭代
 */
private class Itr implements Iterator<E> {
    int cursor = 0;   //将要访问的元素的索引
    int lastRet = -1;  //上一个访问元素的索引
    int expectedModCount = modCount;//expectedModCount为预期修改值,初始化等于modCount(AbstractList类中的一个成员变量)

    //判断是否还有下一个元素
    public boolean hasNext() {
            return cursor != size();
    }
    //取出下一个元素
    public E next() {
            checkForComodification();  //关键的一行代码,判断expectedModCount和modCount是否相等
        try {
        E next = get(cursor);
        lastRet = cursor++;
        return next;
        } catch (IndexOutOfBoundsException e) {
        checkForComodification();
        throw new NoSuchElementException();
        }
    }

    public void remove() {
        if (lastRet == -1)
        throw new IllegalStateException();
            checkForComodification();

        try {
        AbstractList.this.remove(lastRet);
        if (lastRet < cursor)
            cursor--;
        lastRet = -1;
        expectedModCount = modCount;
        } catch (IndexOutOfBoundsException e) {
        throw new ConcurrentModificationException();
        }
    }

    final void checkForComodification() {
        if (modCount != expectedModCount)
        throw new ConcurrentModificationException();
    }
    }

根据代码可知,每次迭代list时,会初始化Itr的三个成员变量

int cursor = 0;   //将要访问的元素的索引
int lastRet = -1;  //上一个访问元素的索引
int expectedModCount = modCount; //预期修改值,初始化等于modCount(AbstractList类中的一个成员变量)

接着调用hasNext()循环判断访问元素的下标是否到达末尾。如果没有,调用next()方法,取出元素。
而最上面测试代码出现异常的原因在于,next()方法调用checkForComodification()时,发现了modCount != expectedModCount

接下来我们看下ArrayList的源码,了解下modCount 是如何与expectedModCount不相等的。

public boolean add(E paramE) {  
    ensureCapacityInternal(this.size + 1);  
    /** 省略此处代码 */  
}  
  
private void ensureCapacityInternal(int paramInt) {  
    if (this.elementData == EMPTY_ELEMENTDATA)  
        paramInt = Math.max(10, paramInt);  
    ensureExplicitCapacity(paramInt);  
}  
  
private void ensureExplicitCapacity(int paramInt) {  
    this.modCount += 1;    //修改modCount  
    /** 省略此处代码 */  
}  
  
public boolean remove(Object paramObject) {  
    int i;  
    if (paramObject == null)  
        for (i = 0; i < this.size; ++i) {  
            if (this.elementData[i] != null)  
                continue;  
            fastRemove(i);  
            return true;  
        }  
    else  
        for (i = 0; i < this.size; ++i) {  
            if (!(paramObject.equals(this.elementData[i])))  
                continue;  
            fastRemove(i);  
            return true;  
        }  
    return false;  
}  
  
private void fastRemove(int paramInt) {  
    this.modCount += 1;   //修改modCount  
    /** 省略此处代码 */  
}  
  
public void clear() {  
    this.modCount += 1;    //修改modCount  
    /** 省略此处代码 */  
}  

从上面的代码可以看出,ArrayList的add、remove、clear方法都会造成modCount的改变。迭代过程中如何调用这些方法就会造成modCount的增加,使迭代类中expectedModCount和modCount不相等。

异常的解决

1. 单线程环境

好,现在我们已经基本了解了异常的发送原因了。接下来我们来解决它。
我很任性,我就是想在迭代集合时删除集合的元素,怎么办?

Iterator<String> iter = list.iterator();
while(iter.hasNext()){
    String str = iter.next();
      if( str.equals("B") )
      {
        iter.remove();
      }
}

细心的朋友会发现Itr中的也有一个remove方法,实质也是调用了ArrayList中的remove,但增加了expectedModCount = modCount;保证了不会抛出java.util.ConcurrentModificationException异常。

但是,这个办法的有两个弊端
1.只能进行remove操作,add、clear等Itr中没有。
2.而且只适用单线程环境。

2. 多线程环境

在多线程环境下,我们再次试验下上面的代码

public class Test2 {
    static List<String> list = new ArrayList<String>();

    public static void main(String[] args) {
        list.add("a");
        list.add("b");
        list.add("c");
        list.add("d");

        new Thread() {
            public void run() {
                Iterator<String> iterator = list.iterator();

                while (iterator.hasNext()) {
                    System.out.println(Thread.currentThread().getName() + ":"
                            + iterator.next());
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            };
        }.start();

        new Thread() {
            public synchronized void run() {
                Iterator<String> iterator = list.iterator();

                while (iterator.hasNext()) {
                    String element = iterator.next();
                    System.out.println(Thread.currentThread().getName() + ":"
                            + element);
                    if (element.equals("c")) {
                        iterator.remove();
                    }
                }
            };
        }.start();

    }
}

Output:


异常的原因很简单,一个线程修改了list的modCount导致另外一个线程迭代时modCount与该迭代器的expectedModCount不相等。

此时有两个办法:

  1. 迭代前加锁,解决了多线程问题,但还是不能进行迭代add、clear等操作
public class Test2 {
    static List<String> list = new ArrayList<String>();

    public static void main(String[] args) {
        list.add("a");
        list.add("b");
        list.add("c");
        list.add("d");

        new Thread() {
            public void run() {
                Iterator<String> iterator = list.iterator();

                synchronized (list) {
                    while (iterator.hasNext()) {
                        System.out.println(Thread.currentThread().getName()
                                + ":" + iterator.next());
                        try {
                            Thread.sleep(1000);
                        } catch (InterruptedException e) {
                            // TODO Auto-generated catch block
                            e.printStackTrace();
                        }
                    }
                }
            };
        }.start();

        new Thread() {
            public synchronized void run() {
                Iterator<String> iterator = list.iterator();

                synchronized (list) {
                    while (iterator.hasNext()) {
                        String element = iterator.next();
                        System.out.println(Thread.currentThread().getName()
                                + ":" + element);
                        if (element.equals("c")) {
                            iterator.remove();
                        }
                    }
                }
            };
        }.start();

    }
}
  1. 采用CopyOnWriteArrayList,解决了多线程问题,同时可以add、clear等操作
public class Test2 {
    static List<String> list = new CopyOnWriteArrayList<String>();

    public static void main(String[] args) {
        list.add("a");
        list.add("b");
        list.add("c");
        list.add("d");

        new Thread() {
            public void run() {
                Iterator<String> iterator = list.iterator();

                    while (iterator.hasNext()) {
                        System.out.println(Thread.currentThread().getName()
                                + ":" + iterator.next());
                        try {
                            Thread.sleep(1000);
                        } catch (InterruptedException e) {
                            // TODO Auto-generated catch block
                            e.printStackTrace();
                        }
                    }
            };
        }.start();

        new Thread() {
            public synchronized void run() {
                Iterator<String> iterator = list.iterator();

                    while (iterator.hasNext()) {
                        String element = iterator.next();
                        System.out.println(Thread.currentThread().getName()
                                + ":" + element);
                        if (element.equals("c")) {
                            list.remove(element);
                        }
                    }
            };
        }.start();

    }
}

CopyOnWriteArrayList也是一个线程安全的ArrayList,其实现原理在于,每次add,remove等所有的操作都是重新创建一个新的数组,再把引用指向新的数组。
由于我用CopyOnWriteArrayList少,这里就不多讨论了,想了解可以看:Java并发编程:并发容器之CopyOnWriteArrayList

深入理解异常—fail-fast机制

到这里,我们似乎已经理解完这个异常的产生缘由了。
但是,仔细思考,还是会有几点疑惑:

  1. 既然modCount与expectedModCount不同会产生异常,那为什么还设置这个变量
  2. ConcurrentModificationException可以翻译成“并发修改异常”,那这个异常是否与多线程有关呢?

我们来看看源码中modCount的注解

/**
     * The number of times this list has been <i>structurally modified</i>.
     * Structural modifications are those that change the size of the
     * list, or otherwise perturb it in such a fashion that iterations in
     * progress may yield incorrect results.
     *
     * <p>This field is used by the iterator and list iterator implementation
     * returned by the {@code iterator} and {@code listIterator} methods.
     * If the value of this field changes unexpectedly, the iterator (or list
     * iterator) will throw a {@code ConcurrentModificationException} in
     * response to the {@code next}, {@code remove}, {@code previous},
     * {@code set} or {@code add} operations.  This provides
     * <i>fail-fast</i> behavior, rather than non-deterministic behavior in
     * the face of concurrent modification during iteration.
     *
     * <p><b>Use of this field by subclasses is optional.</b> If a subclass
     * wishes to provide fail-fast iterators (and list iterators), then it
     * merely has to increment this field in its {@code add(int, E)} and
     * {@code remove(int)} methods (and any other methods that it overrides
     * that result in structural modifications to the list).  A single call to
     * {@code add(int, E)} or {@code remove(int)} must add no more than
     * one to this field, or the iterators (and list iterators) will throw
     * bogus {@code ConcurrentModificationExceptions}.  If an implementation
     * does not wish to provide fail-fast iterators, this field may be
     * ignored.
     */
    protected transient int modCount = 0;

我们注意到,注解中频繁的出现了fail-fast
那么fail-fast(快速失败)机制是什么呢?

“快速失败”也就是fail-fast,它是Java集合的一种错误检测机制。当多个线程对集合进行结构上的改变的操作时,有可能会产生fail-fast机制。记住是有可能,而不是一定。例如:假设存在两个线程(线程1、线程2),线程1通过Iterator在遍历集合A中的元素,在某个时候线程2修改了集合A的结构(是结构上面的修改,而不是简单的修改集合元素的内容),那么这个时候程序就会抛出 ConcurrentModificationException 异常,从而产生fail-fast机制。

看到这里,我们明白了,fail-fast机制就是为了防止多线程修改集合造成并发问题的机制嘛。
之所以有modCount这个成员变量,就是为了辨别多线程修改集合时出现的错误。而java.util.ConcurrentModificationException就是并发异常。
但是单线程使用不单时也可能抛出这个异常。

参考:Java提高篇(三四)-----fail-fast机制

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,560评论 4 361
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,104评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,297评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,869评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,275评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,563评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,833评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,543评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,245评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,512评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,011评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,359评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,006评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,062评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,825评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,590评论 2 273
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,501评论 2 268

推荐阅读更多精彩内容