1.1 問題復現
public void test1() { ArrayList<Integer> arrayList = new ArrayList<>(); for (int i = 0; i < 20; i++) { arrayList.add(Integer.valueOf(i)); } // 復現方法一 Iterator<Integer> iterator = arrayList.iterator(); while (iterator.hasNext()) { Integer integer = iterator.next(); if (integer.intValue() == 5) { arrayList.remove(integer); } } // 復現方法二 iterator = arrayList.iterator(); for (Integer value : arrayList) { Integer integer = iterator.next(); if (integer.intValue() == 5) { arrayList.remove(integer); } } }
1.2、問題原因分析
先來看實現方法一,方法一中使用Iterator遍歷ArrayList, 拋出異常的是iterator.next(),看下Iterator next方法實現源碼
public E next() {
checkForComodification();
int i = cursor;
if (i >= size)
throw new NoSuchElementException();
Object[] elementData = ArrayList.this.elementData;
if (i >= elementData.length)
throw new ConcurrentModificationException();
cursor = i + 1;
return (E) elementData[lastRet = i];
}final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
在next方法中首先調用了checkForComodification方法,該方法會判斷modCount是否等於expectedModCount,不等於就會拋出java.util.ConcurrentModificationExcepiton異常。
我們接下來跟蹤看一下modCount和expectedModCount的賦值和修改。
modCount是ArrayList的一個屬性,繼承自抽象類AbstractList,用於表示ArrayList對象被修改次數。
protected transient int modCount = 0;
整個ArrayList中修改modCount的方法比較多,有add、remove、clear、ensureCapacityInternal等,凡是設計到ArrayList對象修改的都會自增modCount屬性。
在創建Iterator的時候會將modCount賦值給expectedModCount,在遍歷ArrayList過程中,沒有其他地方可以設置expectedModCount了,因此遍歷過程中expectedModCount會一直保持初始值20(調用add方法添加了20個元素,修改了20次)。
final void checkForComodification() { if (modCount != expectedModCount) throw new ConcurrentModificationException(); }
在執行next方法時,遇到modCount != expectedModCount方法,導致拋出異常java.util.ConcurrentModificationException。
明白了拋出異常的過程,但是爲什麼要這麼做呢?很明顯這麼做是爲了阻止程序員在不允許修改的時候修改對象,起到保護作用,避免出現未知異常。
引用網上的一段解釋,點擊查看解釋來源
Iterator 是工作在一個獨立的線程中,並且擁有一個 mutex 鎖。 Iterator 被創建之後會建立一個指向原來對象的單鏈索引表,當原來的對象數量發生變化時,這個索引表的內容不會同步改變。 當索引指針往後移動的時候就找不到要迭代的對象,所以按照 fail-fast 原則 Iterator 會馬上拋出 java.util.ConcurrentModificationException 異常。 所以 Iterator 在工作的時候是不允許被迭代的對象被改變的。但你可以使用 Iterator 本身的方法 remove() 來刪除對象, Iterator.remove() 方法會在刪除當前迭代對象的同時維護索引的一致性。
再來分析下第二種for循環拋異常的原因:
public void forEach(Consumer<? super E> action) {
Objects.requireNonNull(action);
final int expectedModCount = modCount;
@SuppressWarnings("unchecked")
final E[] elementData = (E[]) this.elementData;
final int size = this.size;
for (int i=0; modCount == expectedModCount && i < size; i++) {
action.accept(elementData[i]);
}
if (modCount != expectedModCount) {
throw new ConcurrentModificationException();
}
}
在for循環中一開始也是對expectedModCount採用modCount進行賦值。在進行for循環時每次都會有判定條件modCount == expectedModCount,當執行完arrayList.remove(integer)之後,該判定條件返回false退出循環,然後執行if語句,結果同樣拋出java.util.ConcurrentModificationException異常。
這兩種復現方法實際上都是同一個原因導致的。
1.3 問題解決方案
上述的兩種復現方法都是在單線程運行的,先來說明單線程中的解決方案:
public void test2() {
ArrayList<Integer> arrayList = new ArrayList<>();
for (int i = 0; i < 20; i++) {
arrayList.add(Integer.valueOf(i));
}Iterator<Integer> iterator = arrayList.iterator();
while (iterator.hasNext()) {
Integer integer = iterator.next();
if (integer.intValue() == 5) {
iterator.remove();
}
}
}
這種解決方案最核心的就是調用iterator.remove()方法。我們看看該方法源碼爲什麼這個方法能避免拋出異常
public void remove() {
if (lastRet < 0)
throw new IllegalStateException();
checkForComodification();try {
ArrayList.this.remove(lastRet);
cursor = lastRet;
lastRet = -1;
expectedModCount = modCount;
} catch (IndexOutOfBoundsException ex) {
throw new ConcurrentModificationException();
}
}
在iterator.remove()方法中,同樣調用了ArrayList自身的remove方法,但是調用完之後並非就return了,而是expectedModCount = modCount重置了expectedModCount值,使二者的值繼續保持相等。
針對forEach循環並沒有修復方案,因此在遍歷過程中同時需要修改ArrayList對象,則需要採用iterator遍歷。
上面提出的解決方案調用的是iterator.remove()方法,如果不僅僅是想調用remove方法移除元素,還想增加元素,或者替換元素,是否可以呢?瀏覽Iterator源碼可以發現這是不行的,Iterator只提供了remove方法。
但是ArrayList的內部類ListItr實現了ListIterator接口,並繼承了內部類Iter(實現了Iterator<E>),這些操作都是可以實現的,使用示例如下:
public void test3() {
ArrayList<Integer> arrayList = new ArrayList<>();
for (int i = 0; i < 20; i++) {
arrayList.add(Integer.valueOf(i));
}ListIterator<Integer> iterator = arrayList.listIterator();
while (iterator.hasNext()) {
Integer integer = iterator.next();
if (integer.intValue() == 5) {
iterator.set(Integer.valueOf(6));
iterator.remove();
iterator.add(integer);
}
}
}
二、 多線程情況下的問題分析及解決方案
2.1 問題復現
public void test4() {
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() {
ListIterator<Integer> iterator = arrayList.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() {
ListIterator<Integer> iterator = arrayList.listIterator();
while (iterator.hasNext()) {
System.out.println("thread2 " + iterator.next().intValue());
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
Exception in thread "Thread-0" java.util.ConcurrentModificationException
at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:901)
at java.util.ArrayList$Itr.next(ArrayList.java:851)
at com.snow.ExceptionTest$1.run(ExceptionTest.java:74)
at java.lang.Thread.run(Thread.java:745)Process finished with exit code 0
2.2 問題分析
從上面代碼執行結果可以看出thread2 遍歷結束後,thread1 sleep完1000ms準備遍歷第二個元素,next的時候拋出異常了。我們從時間點分析一下拋異常的原因
時間點 | arrayList.modCount | thread1 iterator.expectedModCount | thread2 iterator.expectedModCount |
thread start,初始化iterator | 20 | 20 | 20 |
thread2.remove()調用之後 | 21 | 20 | 21 |
兩個thread都是使用的同一個arrayList,thread2修改完後modCount = 21,此時thread2的expectedModCount = 21 可以一直遍歷到結束;thread1的expectedModCount仍然爲20,因爲thread1的expectedModCount只是在初始化的時候賦值,其後並未被修改過。因此當arrayList的modCount被thread2修改爲21之後,thread1想繼續遍歷必定會拋出異常了。
在這個示例代碼裏面,兩個thread,每個thread都有自己的iterator,當thread2通過iterator方法修改expectedModCount必定不會被thread1感知到。這個跟ArrayList非線程安全是無關的,即使這裏面的ArrayList換成Vector也是一樣的結果,不信上測試代碼:
public void test5() {
Vector<Integer> vector = new Vector<>();
for (int i = 0; i < 20; i++) {
vector.add(Integer.valueOf(i));
}Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
ListIterator<Integer> iterator = vector.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() {
ListIterator<Integer> iterator = vector.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
Exception in thread "Thread-0" java.util.ConcurrentModificationException
at java.util.Vector$Itr.checkForComodification(Vector.java:1184)
at java.util.Vector$Itr.next(Vector.java:1137)
at com.snow.ExceptionTest$3.run(ExceptionTest.java:112)
at java.lang.Thread.run(Thread.java:745)Process finished with exit code 0
test5()方法執行結果和test4()是相同的,那如何解決這個問題呢?
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();
}