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的array thread1 iterator 初始化的Object數組 thread2 第一次遍歷forEach初始化的Object數組 thread2 第二次遍歷forEach初始化的Object數組
thread start 假設爲A A A /
thread2 調用remove方法之後 假設爲B A A B

 

 

 

 

 

有了這個時間節點表就很清楚了,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 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章