庖丁解牛之ConcurrentModificationException

前言

ConcurrentModificationException 这个异常很常见,如果程序中经常使用集合操作,对这个异常会非常的熟悉,本人在工作中就遇到过两次这个异常,印象非常深刻,为了以后不再范这类问题特此记录一下

出现问题的场景

  • 在多线程中一个线程修改了数据,另外一个线程正在遍历集合数据,就出现了这类的问题,大多数人多知道这类场景
  • 另外一个场景就是在单线程中,在数据迭代的时候修改集合数据出现的问题

多线程场景

import java.util.ArrayList;
import java.util.Iterator;

/**
 * Created by nate on 2018/7/9.
 */
public class Test {

    static ArrayList<Integer> list = new ArrayList<Integer>();

    public static void main(String args[]) {
        list.add(1);
        list.add(2);
        list.add(3);
        list.add(4);
        list.add(5);
        Thread thread1 = new Thread() {
            public void run() {
                Iterator<Integer> iterator = list.iterator();
                while (iterator.hasNext()) {
                    Integer integer = iterator.next();
                    System.out.println(integer);
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            };
        };
        Thread thread2 = new Thread() {
            public void run() {
                Iterator<Integer> iterator = list.iterator();
                while (iterator.hasNext()) {
                    Integer integer = iterator.next();
                    if (integer == 2)
                        iterator.remove();
                }
            };
        };
        thread1.start();
        thread2.start();
    }

}

结果:出现异常了

1
Exception in thread "Thread-0" java.util.ConcurrentModificationException
at java.util.ArrayListItr.checkForComodification(ArrayList.java:901) at java.util.ArrayListItr.next(ArrayList.java:851)
at Test$1.run(Test.java:21)

单线程场景

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

/**
 * Created by nate on 2018/7/9.
 */
public class Test1 {


    public static void main(String args[]) {
        List<Integer> data = new ArrayList<>();
        data.add(1);
        data.add(2);
        data.add(3);
        data.add(4);

        Iterator<Integer> iterator = data.iterator();
        while (iterator.hasNext()) {
            Integer next=iterator.next();
            if(next==2){
                data.remove(2);
            }
        }
    }
}

结果:出现异常

Exception in thread "main" java.util.ConcurrentModificationException
at java.util.ArrayListItr.checkForComodification(ArrayList.java:901) at java.util.ArrayListItr.next(ArrayList.java:851)
at Test1.main(Test1.java:21)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:497)
at com.intellij.rt.execution.application.AppMain.main(AppMain.java:147)

用foreach迭代也是一样的

import java.util.ArrayList;
import java.util.List;

/**
 * Created by nate on 2018/7/9.
 */
public class Test1 {

    public static void main(String args[]) {
        List<Integer> data = new ArrayList<>();
        data.add(1);
        data.add(2);
        data.add(3);
        data.add(4);
        for (Integer integer : data) {
            if (integer == 2) {
                data.remove(integer);
            }
        }
    }
}

结果:出现异常

Exception in thread "main" java.util.ConcurrentModificationException
at java.util.ArrayListItr.checkForComodification(ArrayList.java:901) at java.util.ArrayListItr.next(ArrayList.java:851)
at Test1.main(Test1.java:17)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:497)
at com.intellij.rt.execution.application.AppMain.main(AppMain.java:147)

但是如果用过for循环就么有问题

import java.util.ArrayList;
import java.util.List;

/**
 * Created by nate on 2018/7/9.
 */
public class Test1 {

    public static void main(String args[]) {
        List<Integer> data = new ArrayList<>();
        data.add(1);
        data.add(2);
        data.add(3);
        data.add(4);

        for (int i = 0; i < data.size(); i++) {
            if (data.get(i) == 2) {
                data.remove(data.get(i));
            }
        }
    }
}

结果正常运行,这是为何呢?

问题原因分析,庖丁解牛

要想分析这个问题,我们只能从源码分析,我们先看一下iterator()这个方法的源码

public Iterator<E> iterator() {
    return new Itr();
}

实际上是new了一个Itr对象,我们看一下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;

    public boolean hasNext() {
        return cursor != size;
    }

    @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];
    }

    public void remove() {
        if (lastRet < 0)
            throw new IllegalStateException();
        checkForComodification();

        try {
            ArrayList.this.remove(lastRet);
            cursor = lastRet;
            lastRet = -1;
            expectedModCount = modCount;
        } catch (IndexOutOfBoundsException ex) {
            throw new ConcurrentModificationException();
        }
    }

    @Override
    @SuppressWarnings("unchecked")
    public void forEachRemaining(Consumer<? super E> consumer) {
        Objects.requireNonNull(consumer);
        final int size = ArrayList.this.size;
        int i = cursor;
        if (i >= size) {
            return;
        }
        final Object[] elementData = ArrayList.this.elementData;
        if (i >= elementData.length) {
            throw new ConcurrentModificationException();
        }
        while (i != size && modCount == expectedModCount) {
            consumer.accept((E) elementData[i++]);
        }
        // update once at end of iteration to reduce heap write traffic
        cursor = i;
        lastRet = i - 1;
        checkForComodification();
    }

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

这个类中有这么一个方法checkForComodification,这个方法中抛出了一个异常,这异常就是ConcurrentModificationException用这个异常,接下来我们就看这个异常是哪个地方调用了,在代码中这个异常是next方法调用,我们把分析的重点放到next方法中,在next方法中首先是先调用的checkForComodification(),

这方法的主要逻辑是检测modCount和expectedModCount是否相等,如果不相当就抛出异常,这两个变量是什么意思呢?

modCount变量表示的这个集合被修改的次数,add、remove都会导致modCount变量增加

1.我们分析一下多线程场景出现的问题的情况

thread1中创建了iterator对象,thread2中也创建iterator对象,这个时候这两个iterator中的expectedModCount值是一样的,但是线程2中调用了remove方法,也就modCount这个值改变了,但是这个modCount值不是用volatile修饰的不是多线程可见了,也就是thread1中的iterator对象的中的成员变量expectedModCount还是老的值,这时候在调用next方法发现modCount != expectedModCount然后就报异常了

2.单线程场景问题分析

问题的根源是这行代码 data.remove(2);这个行代码修改了List类中modCount的值,但是这个值并没有同步到Itr 这个对象中,所以在下次迭代的时候就报异常了

foreach 为什么也出现这个问题

因为foreach循环的实现跟iterator迭代是差不多的所以也出现这个问题,先看一下源代码吧

@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();
    }
}

在forEach循环中,首先保存了modCount的值给expectedModCount,然后在for循环中判断 modCount 和expectedModCount的值是否相等,如果相等正常迭代,如果再迭代的过程中改变了modCount的值就抛出异常,我们上面的例子就是在迭代的时候改变了modCount的值

for循环为什么没有出现这个问题呢?

因为for循环中没有加入modCount的值和expectedModCount是否相等这个逻辑,所以可以正常运行

如何避免这类问题?

知道问题的原因后,我们就想怎么解决这类问题

对于多线程请求如何解决?

解决办法有两种:一种是在iterator迭代的时候加锁,确保没有其他线程干扰,另外一种是使用并发容器CopyOnWriteArrayList代替ArrayList和Vector

对于单线程情况

我建议直接用for 索引的方式迭代数据,不要用foreach方式遍历数据,如果有对数据修改的操作,直接调用iterator的remove方法

迭代的时候加锁

import java.util.ArrayList;
import java.util.Iterator;

/**
 * Created by nate on 2018/7/9.
 */
public class Test {

    static ArrayList<Integer> list = new ArrayList<Integer>();

    public static void main(String args[]) {
        list.add(1);
        list.add(2);
        list.add(3);
        list.add(4);
        list.add(5);
        Thread thread1 = new Thread() {
            public void run() {
                Iterator<Integer> iterator = list.iterator();
                synchronized (list) {
                    while (iterator.hasNext()) {
                        Integer integer = iterator.next();
                        System.out.println(integer);
                        try {
                            Thread.sleep(100);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                }
            };
        };
        Thread thread2 = new Thread() {
            public void run() {
                Iterator<Integer> iterator = list.iterator();
                synchronized (list) {
                    while (iterator.hasNext()) {
                        Integer integer = iterator.next();
                        if (integer == 2)
                            iterator.remove();
                    }
                }
            }  ;
        };
        thread1.start();
        thread2.start();
    }

}

程序正常运行,通过加锁的方式保证了同一个时候只能有一个线程访问,所以不会出问题,但是效率也下降了

有的人想如果把ArrayList 换成Vector 是不是也可以呢?答案可能令大家失望了,同样会报错

import java.util.ArrayList;
import java.util.Iterator;
import java.util.Vector;

/**
 * Created by nate on 2018/7/9.
 */
public class Test {

    static Vector<Integer> list = new Vector<Integer>();

    public static void main(String args[]) {
        list.add(1);
        list.add(2);
        list.add(3);
        list.add(4);
        list.add(5);
        Thread thread1 = new Thread() {
            public void run() {
                Iterator<Integer> iterator = list.iterator();
                while (iterator.hasNext()) {
                    Integer integer = iterator.next();
                    System.out.println(integer);
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            };
        };
        Thread thread2 = new Thread() {
            public void run() {
                Iterator<Integer> iterator = list.iterator();
                while (iterator.hasNext()) {
                    Integer integer = iterator.next();
                    if (integer == 2)
                        iterator.remove();
                }
            }

            ;
        };
        thread1.start();
        thread2.start();
    }

}

结果:

1
Exception in thread "Thread-0" java.util.ConcurrentModificationException
at java.util.VectorItr.checkForComodification(Vector.java:1184) at java.util.VectorItr.next(Vector.java:1137)
at Test$1.run(Test.java:22)

也是为什么呢,Vector不是所以的方法都是加锁的吗?Vector的方法是加锁的这个没有错,但是Iterator类的方法可以不加锁的,所以多个线程访问还是会有问题的

CopyOnWriteArrayList 方式

CopyOnWriteArrayList 可以解决迭代的时候崩溃问题,但是要对个这个类有足够的了解才能使用,否则受伤的只是自己

import java.util.Iterator;
import java.util.concurrent.CopyOnWriteArrayList;

/**
 * Created by nate on 2018/7/9.
 */
public class Test {

    static CopyOnWriteArrayList<Integer> list = new CopyOnWriteArrayList<Integer>();

    public static void main(String args[]) {
        list.add(1);
        list.add(2);
        list.add(3);
        list.add(4);
        list.add(5);
        Thread thread1 = new Thread() {
            public void run() {
                Iterator<Integer> iterator = list.iterator();
                while (iterator.hasNext()) {
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    Integer integer = iterator.next();
                    System.out.println(Thread.currentThread().getId()+" : " +integer);
                }
            }

            ;
        };
        Thread thread2 = new Thread() {
            public void run() {
                Iterator<Integer> iterator = list.iterator();
                while (iterator.hasNext()) {
                    Integer integer = iterator.next();
                    if (integer == 2) {
//                        iterator.remove();
                        list.remove(integer);
                    }
                    System.out.println(Thread.currentThread().getId()+" : " +integer);

                }
                System.out.println(Thread.currentThread().getId()+" : " +list);

            }

            ;
        };
        thread1.start();
        thread2.start();
    }
}

结果:

12 : 1
12 : 2
12 : 3
12 : 4
12 : 5
12 : [1, 3, 4, 5]
11 : 1
11 : 2
11 : 3
11 : 4
11 : 5

我们关注一下线程id是12的,线程id是12这个输出的list结果是[1, 3, 4, 5],这个是正确的,因为我们已经移除了2这个数据

但是线程id是11的这个输出的有问题,因为2这个数据我们已经在线程12中已经移除,并且11 : 2这个是在12 : [1, 3, 4, 5]之后打印的,说明数据已经移除了,但是log上会输出11:2这个呢?这个就和CopyOnWriteArrayList的机制有关系了,CopyOnWriteArrayList为了解决ConcurrentModificationException这个异常,在每次在对CopyOnWriteArrayList中的数据进行修改例如add、remove操作的时候,都会创建一个新的Object[] 数组,操作完成后在复制给 CopyOnWriteArrayList中的elements变量,线程11还是访问的旧的数据,所以还会输出2

/**
 * Sets the array.
 */
final void setArray(Object[] a) {
    elements = a;
}

/**
 * Creates an empty list.
 */
public CopyOnWriteArrayList() {
    setArray(new Object[0]);
}

iterator()方法

public Iterator<E> iterator() {
    return new COWIterator<E>(getArray(), 0);
}

getArray方法

final Object[] getArray() {
    return elements;
}
static final class COWIterator<E> implements ListIterator<E> {
    /** Snapshot of the array */
    private final Object[] snapshot;
    /** Index of element to be returned by subsequent call to next.  */
    private int cursor;

    COWIterator(Object[] elements, int initialCursor) {
        cursor = initialCursor;
        snapshot = elements;
    }

    public boolean hasNext() {
        return cursor < snapshot.length;
    }

    public boolean hasPrevious() {
        return cursor > 0;
    }

    @SuppressWarnings("unchecked")
    public E next() {
        if (! hasNext())
            throw new NoSuchElementException();
        return (E) snapshot[cursor++];
    }

    @SuppressWarnings("unchecked")
    public E previous() {
        if (! hasPrevious())
            throw new NoSuchElementException();
        return (E) snapshot[--cursor];
    }

    public int nextIndex() {
        return cursor;
    }

    public int previousIndex() {
        return cursor-1;
    }

    /**
     * Not supported. Always throws UnsupportedOperationException.
     * @throws UnsupportedOperationException always; {@code remove}
     *         is not supported by this iterator.
     */
    public void remove() {
        throw new UnsupportedOperationException();
    }

    /**
     * Not supported. Always throws UnsupportedOperationException.
     * @throws UnsupportedOperationException always; {@code set}
     *         is not supported by this iterator.
     */
    public void set(E e) {
        throw new UnsupportedOperationException();
    }

    /**
     * Not supported. Always throws UnsupportedOperationException.
     * @throws UnsupportedOperationException always; {@code add}
     *         is not supported by this iterator.
     */
    public void add(E e) {
        throw new UnsupportedOperationException();
    }

    @Override
    @SuppressWarnings("unchecked")
    public void forEachRemaining(Consumer<? super E> action) {
        Objects.requireNonNull(action);
        final int size = snapshot.length;
        for (int i = cursor; i < size; i++) {
            action.accept((E) snapshot[i]);
        }
        cursor = size;
    }
}

总结:

1.COWIterator在创建的时候保留了CopyOnWriteArrayList的数组,之后都访问的这个数据,不关CopyOnWriteArrayList中的数据是否有变化

2.COWIterator 不支持remove、add方法

3.每次修改都需要重新new一个数组,并且将array数组数据拷贝到new出来的数组中,效率会大幅下降

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

推荐阅读更多精彩内容