基本上所有的集合類都會有一個叫做快速失敗的校驗機制,當一個集合在被多個線程修改並訪問時,就會出現ConcurrentModificationException 校驗機制。它的實現原理就是我們經常提到的modCount修改計數器。如果在讀列表時,modCount發生變化則會拋出ConcurrentModificationException異常。這與線程同步是兩碼事,線程同步是為了保護集合中的數據不被臟讀、臟寫而設置的。
1、單線程環境下的異常重現
public static void main(String[] args) { ArrayList<Integer> list = new ArrayList<Integer>(); list.add(2); Iterator<Integer> iterator = list.iterator(); while(iterator.hasNext()){ Integer integer = iterator.next(); if(integer==2) { list.remove(integer); //iterator.remove(); //正確寫法 } } }
在while(iterator.hasNext()) 循環遍歷時,只允許刪除ArrayList 內部的 elementData[ ] 的最后一個元素,而不允許從中間刪除。
在 iterator.next() 的源碼中,會首先執行方法:checkForComodification(); 該方法:
final void checkForComodification() { if (modCount != expectedModCount) throw new ConcurrentModificationException(); }
只有當兩個變量值相等時才不會報錯。而 list.add(2)操作和 list.remove(integer); 操作會使 modCount++; 但是變量 expectedModCount 是內部類的 Itr 子類 的 變量,該子類的實例化方法是:list.iterator(); 實例對象時賦初始值:
int expectedModCount = modCount; 所以,不允許在 while(iterator.hasNext()) 循環方法內 有list.add(2)操作和 list.remove(integer);操作,否則會導致expectedModCount != modCount ,進而導致 throw new ConcurrentModificationException();
2、多線程下的問題重現:
public class Vector { public static void main(String[] args) { final List<String> tickets = new ArrayList<String>(); for(int i=0;i<100000;i++){ tickets.add("火車票"+i); } Thread returnThread = new Thread(){ @Override public void run() { while (true){ tickets.add("車票"+ new Random().nextInt()); } }; }; Thread saleThread = new Thread(){ @Override public void run() { for (String ticket : tickets){ tickets.remove(0); } }; }; returnThread.start(); saleThread.start(); } }
有可能有朋友說ArrayList是非線程安全的容器,換成Vector就沒問題了,實際上換成Vector還是會出現這種錯誤。
原因在於,雖然Vector的方法采用了synchronized進行了同步,但是實際上通過Iterator訪問的情況下,每個線程里面返回的是不同的iterator,也即是說expectedModCount是每個線程私有。假若此時有2個線程,線程1在進行遍歷,線程2在進行修改,那么很有可能導致線程2修改后導致Vector中的modCount自增了,線程2的expectedModCount也自增了,但是線程1的expectedModCount沒有自增,此時線程1遍歷時就會出現expectedModCount不等於modCount的情況了。
因此一般有2種解決辦法:
1)在使用iterator迭代的時候使用synchronized或者Lock進行同步;
2)使用並發容器CopyOnWriteArrayList代替ArrayList和Vector。