如何解决java.util.ConcurrentModificationException问题

问题

最近在debug的时候,莫名奇妙的会遇到java.util.ConcurrentModificationException问题。

根据我的历史经验,发生这种问题肯定是for循环里调了remove或者add。

但是看了一圈代码,没发现此类操作,有点蒙圈,这是为啥。。。

根因分析

平常遇到的java.util.ConcurrentModificationException大多是下面的第一种,迭代器遍历和集合类的add/remove方法同时调用了。

像增强for循环底层也属于迭代器遍历,所以这种错误是比较常见的。

我这次遇到的就真的是多线程场景下的并发修改错误。

并发修改错误原因分析

java.util.ArrayList.Itr是ArrayList的内部类,expectedModCount是属于Itr的成员变量,。

modCount是java.util.AbstractList的成员变量。

首先看一下Itr类的定义

   /**
     * 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;

        Itr() {}

        public boolean hasNext() {
            return cursor != size;
        }
       //省略其他代码
    }

在生成迭代器Itr的时候,expectedModCount相当于是拿的当前ArrayList的modCount的值。

后续list.add()或者list.remove()只会修改modCount,expectedModCount是不会受list.add()或者list.remove()影响的。

再进行迭代器遍历的时候就会抛出java.util.ConcurrentModificationException

迭代器和集合类方法同时使用

java的集合类有如下2个字段, 翻译过来就是expectedModCount!=modCount的时候,会抛出并发异常。

期望是修改的数量和期望值相同的,不同的时候肯定是有问题了。

/**
 * 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;

像如下这种写法,肯定会抛出java.util.ConcurrentModificationException异常的。

因为for循环底层是使用的迭代器,这种情况就会导致并发修改错误。

public static void main(String[] args) {
        List<String> list = new ArrayList<>();

        list.add("a");
        list.add("b");
        list.add("c");
        list.add("d");
        list.add("e");

        for (String a : list) {
            System.out.println(a);
            list.remove(0);
        }
}

这段代码编译为class,结果如下, 可以看到for循环变成了iterator迭代遍历。

遍历是使用的iterator,但是remove方法是集合类的自己的方法。

 public static void main(String[] args) {
        List<String> list = new ArrayList();
        list.add("a");
        list.add("b");
        list.add("c");
        list.add("d");
        list.add("e");
        Iterator var2 = list.iterator();

        while(var2.hasNext()) {
            String a = (String)var2.next();
            System.out.println(a);
            list.remove(0);
        }
}

java.util.ArrayList.Itr#next迭代器的next会先校验modCount != expectedModCount, 2个值不相等就抛出异常

@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];
}
final void checkForComodification() {
    if (modCount != expectedModCount)
        throw new ConcurrentModificationException();
}

正常使用迭代器的话modCount == expectedModCount,这种情况是不会抛异常的。

但是list.remove(0),会修改modCount,而不修expectedModCount,导致下一次迭代报错

public E remove(int index) {
       rangeCheck(index);

       modCount++;
       E oldValue = elementData(index);

       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

       return oldValue;
}

总结一下就是:

迭代器遍历不能和集合类自身的add/remove方法一起调用,这样会导致modCount和expectedModCount不相等,从而抛出java.util.ConcurrentModificationException

add/remove都是集合类自身的方法,都只修改modCount而不修改expectedModCount

并发修改导致的异常

之前遇到的都是上面一种导致的异常,这次真的就遇到多线程场景下的java.util.ConcurrentModificationException了。

看一下下面的代码

 public static void main(String[] args) {
        List<String> list = new ArrayList<>();

        list.add("a");
        list.add("b");
        list.add("c");
        list.add("d");
        list.add("e");

        Thread thread = new Thread(() -> list.forEach(a -> { System.out.println(a); }));
        thread.start();

        Thread thread1 = new Thread(() -> list.sort((o1, o2) -> o2.length() - o1.length()));
        thread1.start();
}

这段代码直接执行是没有问题的,可以正常结束,但是稍微修改一下,加点延迟,就会有问题了

   public static void main(String[] args) {
        List<String> list = new ArrayList<>();

        list.add("a");
        list.add("b");
        list.add("c");
        list.add("d");
        list.add("e");

        Thread thread = new Thread(() -> list.forEach(a -> {
            try {
                Thread.sleep(50);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            System.out.println(a);
        }));
        thread.start();

        Thread thread1 = new Thread(() -> list.sort((o1, o2) -> o2.length() - o1.length()));
        thread1.start();
    }

//运行异常
Exception in thread "Thread-0" java.util.ConcurrentModificationException
    at java.util.ArrayList.forEach(ArrayList.java:1262)
    at com.fc.se.list.ListTest.lambda$main$1(ListTest.java:23)
    at java.lang.Thread.run(Thread.java:750)

看异常堆栈是java.util.ArrayList#forEach方法, 会校验modCount != expectedModCount,不符合预期就报错

 @Override
    public void forEach(Consumer<? super E> action) {
        Objects.requireNonNull(action);
        final int expectedModCount = modCount;
        @SuppressWarnings("unchecked")
        final E[] elementData = (E[]) this.elementData;
        final int size = this.size;
        for (int i=0; modCount == expectedModCount && i < size; i++) {
            action.accept(elementData[i]);
        }
        if (modCount != expectedModCount) {
            throw new ConcurrentModificationException();
        }
    }

现在这个场景变成了在2个线程里对同一个list进行遍历和sort操作.

看下sort操作

 @Override
 @SuppressWarnings("unchecked")
 public void sort(Comparator<? super E> c) {
     final int expectedModCount = modCount;
     Arrays.sort((E[]) elementData, 0, size, c);
     if (modCount != expectedModCount) {
     throw new ConcurrentModificationException();
     }
     modCount++;
 }

这2个代码放一起比较一下就可以看出端倪了,sort()会修改modeCount, 但是不会修改expectedModCount。

再次进行list.foreach()时,由于modCount != expectedModCount,就会抛出ConcurrentModificationException

如果不加Thread.sleep(50);,thread会迅速执行完成,相当于2个线程串行执行,所以不会有并发修改问题。

加了Thread.sleep(50);,2个线程会并发执行,就会抛异常了。

解决方式

第一种,避免在迭代器里执行add/remove操作,如果需要在遍历的过程中修改集合,记得使用迭代器进行操作

第二种也可以归类为迭代器和集合的操作,这种首先也需要避免迭代器和遍历一起操作, 另外多线程需要确保线程安全,按实际情况加锁。

Fail-Fast

上面的栗子就是fail-fast的一种场景,不符合预期,直接报错。

什么是fail-fast

首先我们看下维基百科中关于fail-fast的解释:

In systems design, a fail-fast system is one which immediately reports at its interface any condition that is likely to indicate a failure. Fail-fast systems are usually designed to stop normal operation rather than attempt to continue a possibly flawed process. Such designs often check the system's state at several points in an operation, so any failures can be detected early. The responsibility of a fail-fast module is detecting errors, then letting the next-highest level of the system handle them.

大概意思是:在系统设计中,快速失效系统一种可以立即报告任何可能表明故障的情况的系统。

快速失效系统通常设计用于停止正常操作,而不是试图继续可能存在缺陷的过程。

这种设计通常会在操作中的多个点检查系统的状态,因此可以及早检测到任何故障。

快速失败模块的职责是检测错误,然后让系统的下一个最高级别处理错误。

其实,这是一种理念,说白了就是在做系统设计的时候先考虑异常情况,一旦发生异常,直接停止并上报。

Fail-Safe

与之相对的还有fail-safe,这是一种并发安全机制。

为了避免触发fail-fast机制,导致异常,我们可以使用Java中提供的一些采用了fail-safe机制的集合类。

这样的集合容器在遍历时不是直接在集合内容上访问的,而是先复制原有集合内容,在拷贝的集合上进行遍历。

java.util.concurrent包下的容器都是fail-safe的,可以在多线程下并发使用,并发修改。同时也可以在foreach中进行add/remove 。

参考文章

一不小心就踩坑的fail-fast是个什么鬼?

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

推荐阅读更多精彩内容