java 多線程操作List,已經做了同步synchronized,還會有ConcurrentModificationException,知道為什么嗎?


如題,最近項目里有個模塊我做了異步處理方面的事情,在code過程中發現一個顛覆我對synchronized這個關鍵字和用法的地方,請問各位java開發者們是否對此有一個合理的解釋,不多說,我直接貼出問題代碼:

 

(事實證明這是一個坑,各位讀者,如果有興趣,可以先不看答案,自己看看能不能發現這個坑)

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;

public class ConcurrentList {
    //private static List<String> TEST_LIST = new CopyOnWriteArrayList<String>();
    private static List<String> TEST_LIST = Collections.synchronizedList(new ArrayList<String>());

    public static void main(String[] args) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                while (true) {
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    synchronized (TEST_LIST) {
                        TEST_LIST.add("11");
                    }
                    System.out.println("Thread1 running");
                }
            }
        }).start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                while (true) {
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    synchronized (TEST_LIST) {
                        for (String at : TEST_LIST) {
                            TEST_LIST.add("22");
                        }
                    }
                    System.out.println("Thread2 running");
                }
            }
        }).start();
    }
}

輸出結果是:

Thread1 running
Exception in thread "Thread-1" java.util.ConcurrentModificationException
    at java.util.AbstractList$Itr.checkForComodification(AbstractList.java:372)
    at java.util.AbstractList$Itr.next(AbstractList.java:343)
    at com.free4lab.lol.ConcurrentList$2.run(ConcurrentList.java:40)
    at java.lang.Thread.run(Thread.java:619)
Thread1 running
Thread1 running
Thread1 running
Thread1 running
Thread1 running
Thread1 running
Thread1 running
Thread1 running

 

 

 

-----------------------------------分隔線,以下是解釋--------------------------------

 

 

 

問題明了了:

以上問題不是並發的問題,是ArrayList的問題,是個坑!且看如下代碼,以及運行結果:

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;

public class ConcurrentList {
    //private static List<String> TEST_LIST = new CopyOnWriteArrayList<String>();
    private static List<String> TEST_LIST = Collections.synchronizedList(new ArrayList<String>());

    public static void main(String[] args) {
        TEST_LIST.add("111");
        TEST_LIST.add("222");
        for (String at : TEST_LIST) {
            System.out.println(at);
            TEST_LIST.add("333");
            System.out.println("add over");
        }
    }
}

結果是:

111
add over
Exception in thread "main" java.util.ConcurrentModificationException
    at java.util.AbstractList$Itr.checkForComodification(AbstractList.java:372)
    at java.util.AbstractList$Itr.next(AbstractList.java:343)
    at com.free4lab.lol.ConcurrentList.main(ConcurrentList.java:15)

分析:我們發現迭代了一次之后就拋出所謂的並發修改異常,不過這里沒有多線程,看下源代碼就知道了

list.add的時候執行了,修改了modCount,循環外面一次add到第一次迭代不會有問題,因為初始化的時候在AbstractList中int expectedModCount = modCount;,

/**
     * Appends the specified element to the end of this list.
     *
     * @param e element to be appended to this list
     * @return <tt>true</tt> (as specified by {@link Collection#add})
     */
    public boolean add(E e) {
    ensureCapacity(size + 1);  // Increments modCount!!
    elementData[size++] = e;
    return true;
    }

public void ensureCapacity(int minCapacity) {
    modCount++;
    int oldCapacity = elementData.length;
    if (minCapacity > oldCapacity) {
        Object oldData[] = elementData;
        int newCapacity = (oldCapacity * 3)/2 + 1;
            if (newCapacity < minCapacity)
        newCapacity = minCapacity;
            // minCapacity is usually close to size, so this is a win:
            elementData = Arrays.copyOf(elementData, newCapacity);
    }
    }

public E next() {
            checkForComodification();
        try {
        E next = get(cursor);
        lastRet = cursor++;
        return next;
        } catch (IndexOutOfBoundsException e) {
        checkForComodification();
        throw new NoSuchElementException();
        }
    }

這樣迭代器next()第一次 checkForComodification() 是不會拋出異常的,第二次才會拋出異常,因為在checkForComodification()里檢查了

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

這樣,在循環迭代中,進行了一次add操作,修改了modcount變量,再次迭代的時候,異常就throw出來了!

 

如果非要進行這樣的操作,那么聲明list為CopyOnWriteArrayList,就ok!因為用了copyonwrite技術

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;

public class ConcurrentList {
    private static List<String> TEST_LIST = new CopyOnWriteArrayList<String>();
    //private static List<String> TEST_LIST = Collections.synchronizedList(new ArrayList<String>());

    public static void main(String[] args) {
        TEST_LIST.add("111");
        TEST_LIST.add("222");
        for (String at : TEST_LIST) {
            System.out.println(at);
            TEST_LIST.add("333");
            System.out.println("add over");
        }
    }
}

輸出是正確的:

111
add over
222
add over

 

額外再說一點,也可以用iterator迭代,不過同樣也無法調用next()方法(我注釋掉了),這樣程序就是死循環了,不斷的加,不斷的迭代。所以我感覺如果需要在迭代中增加元素,真正有用的還是CopyOnWriteArrayList,不過實際中,如果CopyOnWriteArrayList代價太高,可能我們可以申請一個臨時list存放,在迭代后合並到主list中!

import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;

public class ConcurrentList {
    //private static List<String> TEST_LIST = new CopyOnWriteArrayList<String>();
    private static List<String> TEST_LIST = Collections.synchronizedList(new ArrayList<String>());

    public static void main(String[] args) {
        TEST_LIST.add("111");
        TEST_LIST.add("222");
        Iterator iterator  = TEST_LIST.iterator();  
        while(iterator.hasNext()){
            //System.out.println(iterator.next());
            TEST_LIST.add("333");
            System.out.println("add over");
        }  
    }
}

 

萌萌的IT人,IT人的樂園


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM