java.util.ConcurrentModificationException 异常问题(二)

上一篇接:https://blog.csdn.net/wsen1229/article/details/103288769

2.3 多线程下的解决方案

2.3.1 方案一:iterator遍历过程加同步锁,锁住整个arrayList

public static void test5() {
        ArrayList<Integer> arrayList = new ArrayList<>();
        for (int i = 0; i < 20; i++) {
            arrayList.add(Integer.valueOf(i));
        }

        Thread thread1 = new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (arrayList) {
                    ListIterator<Integer> iterator = arrayList.listIterator();
                    while (iterator.hasNext()) {
                        System.out.println("thread1 " + iterator.next().intValue());
                        try {
                            Thread.sleep(100);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                }
            }
        });

        Thread thread2 = new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (arrayList) {
                    ListIterator<Integer> iterator = arrayList.listIterator();
                    while (iterator.hasNext()) {
                        Integer integer = iterator.next();
                        System.out.println("thread2 " + integer.intValue());
                        if (integer.intValue() == 5) {
                            iterator.remove();
                        }
                    }
                }
            }
        });
        thread1.start();
        thread2.start();
    }

 这种方案本质上是将多线程通过加锁来转变为单线程操作,确保同一时间内只有一个线程去使用iterator遍历arrayList,其它线程等待,效率显然是只有单线程的效率。

2.3.2 方案二:使用CopyOnWriteArrayList,有坑!要明白原理再用,否则你就呆坑里吧。

public void test6() {
        List<Integer> list = new CopyOnWriteArrayList<>();
        for (int i = 0; i < 20; i++) {
            list.add(Integer.valueOf(i));
        }

        Thread thread1 = new Thread(new Runnable() {
            @Override
            public void run() {
                ListIterator<Integer> iterator = list.listIterator();
                while (iterator.hasNext()) {
                    System.out.println("thread1 " + iterator.next().intValue());
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        });

        Thread thread2 = new Thread(new Runnable() {
            @Override
            public void run() {
                for (Integer integer : list) {
                    System.out.println("thread2 " + integer.intValue());
                    if (integer.intValue() == 5) {
                        list.remove(integer);
                    }
                }
                for (Integer integer : list) {
                    System.out.println("thread2 again " + integer.intValue());
                }
//                ListIterator<Integer> iterator = list.listIterator();
//                while (iterator.hasNext()) {
//                    Integer integer = iterator.next();
//                    System.out.println("thread2 " + integer.intValue());
//                    if (integer.intValue() == 5) {
//                        iterator.remove();
//                    }
//                }
            }
        });
        thread1.start();
        thread2.start();
    }

 先不分析,看执行结果,这个执行结果重点关注字体加粗部分。

 

thread1 0
thread2 0
thread2 1
thread2 2
thread2 3
thread2 4
thread2 5
thread2 6
thread2 7
thread2 8
thread2 9
thread2 10
thread2 11
thread2 12
thread2 13
thread2 14
thread2 15
thread2 16
thread2 17
thread2 18
thread2 19
thread2 again 0
thread2 again 1
thread2 again 2
thread2 again 3
thread2 again 4
thread2 again 6
thread2 again 7
thread2 again 8
thread2 again 9
thread2 again 10
thread2 again 11
thread2 again 12
thread2 again 13
thread2 again 14
thread2 again 15
thread2 again 16
thread2 again 17
thread2 again 18
thread2 again 19
thread1 1
thread1 2
thread1 3
thread1 4
thread1 5
thread1 6
thread1 7
thread1 8
thread1 9
thread1 10
thread1 11
thread1 12
thread1 13
thread1 14
thread1 15
thread1 16
thread1 17
thread1 18
thread1 19

Process finished with exit code 0

我们先分析thread2的输出结果,第一次遍历将4 5 6都输出,情理之中;第一次遍历后删除掉了一个元素,第二次遍历输出4 6,符合我们的预期。

再来看下thread1的输出结果,有意思的事情来了,thread1 仍然输出了4 5 6,什么鬼?thread1和thread2都是遍历list,list在thread1遍历第二个元素的时候就已经删除了一个元素了,为啥还能输出5?

为了了解这个问题,需要了解CopyOnWriteArrayList是如何做到一边遍历的同时还能一边修改并且还不抛异常的。

在这里不想再深入分析CopyOnWriteArrayList代码,后续会专门出一篇博客来解释这个类的源码的。

这里说一下CopyOnWriteArrayList的解决思路,其实很简单:

private transient volatile Object[] array;

 

CopyOnWriteArrayList本质上是对array数组的一个封装,一旦CopyOnWriteArrayList对象发生任何的修改都会new一个新的Object[]数组newElement,在newElement数组上执行修改操作,修改完成后将newElement赋值给array数组(array=newElement)。

因为array是volatile的,因此它的修改对所有线程都可见。

了解了CopyOnWriteArrayList的实现思路之后,我们再来分析上面代码test6为什么会出现那样的输出结果。先来看下thread1和thread2中用到的两种遍历方式的源码:

public void forEach(Consumer<? super E> action) {
        if (action == null) throw new NullPointerException();
        // 在遍历开始前获取当前数组
        Object[] elements = getArray();
        int len = elements.length;
        for (int i = 0; i < len; ++i) {
            @SuppressWarnings("unchecked") E e = (E) elements[i];
            action.accept(e);
        }
    }

 

public ListIterator<E> listIterator() {
        return new COWIterator<E>(getArray(), 0);
    }
    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;

        private COWIterator(Object[] elements, int initialCursor) {
            cursor = initialCursor;
            // 初始化为当前数组
            snapshot = elements;
        }

        public void remove() {
            // 已经不支持Iterator remove操作了!!
            throw new UnsupportedOperationException();
        }

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

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

        // 此处省略其他无关代码
    }

 这两种遍历方式有个共同的特点:都在初始化的时候将当前数组保存下来了,之后的遍历都将会遍历这个数组,而不管array如何变化。

时间点CopyOnWriteArrayList的arraythread1 iterator 初始化的Object数组thread2 第一次遍历forEach初始化的Object数组thread2 第二次遍历forEach初始化的Object数组
thread start假设为AAA/
thread2 调用remove方法之后假设为BAAB

 

 

 

 

 

有了这个时间节点表就很清楚了,thread1和thread2 start的时候都会将A数组初始化给自己的临时变量,之后遍历的也都是这个A数组,而不管CopyOnWriteArrayList中的array发生了什么变化。因此也就解释了thread1在thread2 remove掉一个元素之后为什么还会输出5了。在thread2中,第二次遍历初始化数组变成了当前的array,也就是修改后的B,因此不会有Integer.valueOf(5)这个元素了。 

从test6执行结果来看,CopyOnWriteArrayList确实能解决一边遍历一边修改并且还不会抛异常,但是这也是有代价的:

(1) thread2对array数组的修改thread1并不能被动感知到,只能通过hashCode()方法去主动感知,否则就会一直使用修改前的数据

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

此外CopyOnWriteArrayList中的ListIterator实现是不支持remove、add和set操作的,一旦调用就会抛出UnsupportedOperationException异常,因此test6注释代码34-41行中如果运行是会抛异常的。

参考文献: 

http://lz12366.iteye.com/blog/675016 

http://www.cnblogs.com/dolphin0520/p/3933551.html

http://blog.csdn.net/androiddevelop/article/details/21509345 

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值