如題,最近項目里有個模塊我做了異步處理方面的事情,在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"); } } }