1. 程式人生 > 其它 >淺析Java對集合進行操作時報java.util.ConcurrentModificationException併發修改異常問題:產生原因、單執行緒/多執行緒環境解決、CopyOnWriteArrayList執行緒安全的ArrayList、fail-fast快速失敗機制防止多執行緒修改集合造成併發問題

淺析Java對集合進行操作時報java.util.ConcurrentModificationException併發修改異常問題:產生原因、單執行緒/多執行緒環境解決、CopyOnWriteArrayList執行緒安全的ArrayList、fail-fast快速失敗機制防止多執行緒修改集合造成併發問題

一、異常產生

  當我們迭代一個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); } }
*/ } }

二、異常原因

  ArrayList的父類AbstarctList中有一個域 modCount,每次對集合進行修改(增添元素,刪除元素……)時都會 modCount++,而 foreach 的背後實現原理其實就是Iterator(關於Iterator可以看Java Design Pattern: Iterator),等同於註釋部分程式碼。在這裡,迭代ArrayList的Iterator中有一個變數 expectedModCount,該變數會初始化和 modCount 相等,但如果接下來如果集合進行修改 modCount 改變,就會造成 expectedModCount!=modCount

,此時就會丟擲java.util.ConcurrentModificationException異常。過程如下圖:

  我們再來根據原始碼詳細的走一遍這個過程

/*
 *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的三個成員變數:cursorlastRe、expectedModCount = modCount;

  接著呼叫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、單執行緒環境 —— 單執行緒可以使用 迭代器的 remove 方法

  現在我們已經基本瞭解了異常的發生原因了。接下來我們來解決它。我很任性,我就是想在迭代集合時刪除集合的元素,怎麼辦?

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、多執行緒環境 —— 需要使用 CopyOnWriteArrayList

  在多執行緒環境下,我們再次試驗下上面的程式碼

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

  異常的原因很簡單,一個執行緒修改了list的modCount導致另外一個執行緒迭代時modCount與該迭代器的expectedModCount不相等。

  此時有兩個辦法:

(1)迭代前加鎖,解決了多執行緒問題,但還是不能進行迭代add、clear等操作

(2)採用 CopyOnWriteArrayList,解決了多執行緒問題,同時可以add、clear等操作

static List<String> list = new CopyOnWriteArrayList<String>();
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就是併發異常。但是單執行緒使用不當時也可能丟擲這個異常。

原文連結:https://www.jianshu.com/p/c5b52927a61a