最近寫了一個bug就是在遍歷list的時候刪除了裏面一個元素,其實以前看過阿里的java開發規範,知道在遍歷的時候刪除元素會產生問題,可是寫的快的時候仍是會沒注意到,那正好研究下里面的機制看。咱們看看阿里規範怎麼寫的:
java
首先提出一個概念:fail-fast
摘自百度百科:
fail-fast 機制是java集合(Collection)中的一種錯誤機制。當多個線程對同一個集合的內容進行操做時,就可能會產生fail-fast事件。
例如:當某一個線程A經過iterator去遍歷某集合的過程當中,若該集合的內容被其餘線程所改變了;那麼線程A訪問集合時,就會拋出ConcurrentModificationException異常,產生fail-fast事件。
簡單來講是java爲了防止出現併發異常的一個機制,可是其實在單線程下也能夠產生。多線程
接下來,我會經過6個例子來探究下,遍歷list時刪除元素會發生什麼,和它的源碼探究。
前提先造了一個list:併發
private List<String> list = new ArrayList<String>() {{ add("元素1"); add("元素2"); add("元素3"); }};
public void test1() { for (int i = 0; i < list.size() - 1; i++) { if ("元素3".equals(list.get(i))) { System.out.println("找到元素3了"); } if ("元素2".equals(list.get(i))) { list.remove(i); } } } // 這裏不會輸出找到元素3 由於遍歷到元素2的時候刪除了元素2 list的size變小了 // 因此就產生問題了
public void test2() { for (int i = 0; i < list.size() - 1; i++) { if ("元素2".equals(list.get(i))) { list.remove(i); } if ("元素3".equals(list.get(i))) { System.out.println("找到元素3了"); } } } // 這裏會輸出元素3 可是實際上是在遍歷到元素2的時候輸出的 // 遍歷到元素2 而後刪除 到了判斷元素3的條件的時候i是比原來小了1 // 因此又陰差陽錯的輸出了正確結果
public void test3() { for (String item : list) { if ("元素2".equals(item)) { list.remove(item); } if ("元素3".equals(item)) { System.out.println("找到元素3了"); } } } // 這裏和上面的結果有點不同 可是仍是沒有輸出元素3的打印語句 // 這裏反編譯下java文件就能夠知道是爲啥啦 public void test3() { Iterator var1 = this.list.iterator(); while(var1.hasNext()) { // 爲了顯示區別這裏 String var2 = (String)var1.next(); if ("元素2".equals(var2)) { this.list.remove(var2); } if ("元素3".equals(var2)) { System.out.println("找到元素3了"); } } }
反編譯的文件能夠知道這裏加強for反編譯是用了迭代器,來判斷是否還有元素,而後remove用的是list的方法。
咱們看下ArrayList中的hasNext(),next()是怎麼寫的。下面是ArrayList中的內部類Itr實現了Iterator接口,ide
private class Itr implements Iterator<E> { int cursor; // index of next element to return int lastRet = -1; // index of last element returned; -1 if no such int expectedModCount = modCount; Itr() {} public boolean hasNext() { return cursor != size; } @SuppressWarnings("unchecked") 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]; } 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(); } } @Override @SuppressWarnings("unchecked") public void forEachRemaining(Consumer<? super E> consumer) { Objects.requireNonNull(consumer); final int size = ArrayList.this.size; int i = cursor; if (i >= size) { return; } final Object[] elementData = ArrayList.this.elementData; if (i >= elementData.length) { throw new ConcurrentModificationException(); } while (i != size && modCount == expectedModCount) { consumer.accept((E) elementData[i++]); } // update once at end of iteration to reduce heap write traffic cursor = i; lastRet = i - 1; checkForComodification(); } final void checkForComodification() { if (modCount != expectedModCount) throw new ConcurrentModificationException(); } }
這裏cursor在next()執行時,cursor = i + 1,因此當運行到"元素2".equal(item)這裏,元素2被移除,當遍歷當元素3的時候size = 2.cursor = i + 1 = 1 + 1 也是2.
hasNext()中cursor == size就直接退出了。ui
public void test4() { list.forEach( item -> { if ("元素2".equals(item)) { list.remove(item); } if ("元素3".equals(item)) { System.out.println("找到元素3了"); } } ); } // 這裏拋出了咱們期待已經的fail-fast java.util.ConcurrentModificationException
點進去看下ArrayList的源碼this
@Override 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(); } }
根據報錯信息咱們能夠知道是
if (modCount != expectedModCount) {spa
throw new ConcurrentModificationException();
}
拋出的異常,那麼這個modCount元素是從哪裏來的?
咱們找到AbstractList,這個是ArrayList的父類。
看看源碼裏面是怎麼說的線程
/** * 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;
英文好的同窗能夠本身看下,英文很差的同窗能夠默默打開谷歌翻譯或者有道詞典了。翻譯
剩下的同窗能夠聽一下個人理解,其實只看第一段基本上就知道它是幹嗎的了。意思是這個字段是記錄了list的結構性修改次數(咱們能夠理解爲add,remove這些會改變list大小的操做)。若是是其餘方式致使的就回拋出ConcurrentModificationException異常。
因此咱們再去看看子類的ArrayList的add,remove方法。code
// 這個是add裏面的子方法 private void ensureExplicitCapacity(int minCapacity) { modCount++; // overflow-conscious code if (minCapacity - elementData.length > 0) grow(minCapacity); } // 這個是remove(int index) public E remove(int index) { rangeCheck(index); modCount++; E oldValue = elementData(index); int numMoved = size - index - 1; if (numMoved > 0) System.arraycopy(elementData, index+1, elementData, index, numMoved); elementData[--size] = null; // clear to let GC do its work return oldValue; } // 這是remove(Object o) private void fastRemove(int index) { modCount++; int numMoved = size - index - 1; if (numMoved > 0) System.arraycopy(elementData, index+1, elementData, index, numMoved); elementData[--size] = null; // clear to let GC do its work }
看完這幾個方法,是否是都看到了modCount++這個操做?是的,添加和刪除的時候都會對這個modCount加一。好了,咱們在回頭看看爲啥test4()會拋出異常。
首先咱們知道list裏面add了三個元素因此modCount在添加完元素的時候是3,而後它開始遍歷,當發現元素2的時候,去移除了元素2,此時modCount變爲4.
咱們在會到ArrayList的
@Override 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(); } }
能夠看到一開始把modCoun賦值給了expectedModCount,而後for循環裏面和最後的if條件均對這個modCount有跑斷,if裏面若是發現modCount和expectedModCount不相等了,就拋出異常。當遍歷到元素2,action.accept(elementData[i]);這一行執行完,再去遍歷元素3的時候由於modCount == expectedModCount不相等了,因此循環推出,所以不會打印出找到元素3了,而且執行到if條件直接拋出異常。
咱們在來看看正確的使用方式,使用迭代器
public void test5() { Iterator<String> iterator = list.iterator(); while (iterator.hasNext()) { String temp = iterator.next(); if ("元素2".equals(temp)) { iterator.remove(); } if ("元素3".equals(temp)) { System.out.println("找到元素3了"); } } } // 這裏打印出了 找到元素3了
上面的test3咱們已經看到這個迭代器的代碼了,那爲啥它沒問題呢?其實原理很是的沙雕,不信你看!
這是迭代器的remove方法
public void remove() { if (lastRet < 0) throw new IllegalStateException(); checkForComodification(); try { ArrayList.this.remove(lastRet); cursor = lastRet; lastRet = -1; expectedModCount = modCount; // 看到沒有直接把modeCount從新賦值給了expectedModCount 因此它們一直會相等啊 } catch (IndexOutOfBoundsException ex) { throw new ConcurrentModificationException(); } }
因此它是能夠找到元素3的,由於直接忽略了remove對modCount的影響。
jdk8還出了一種新寫法,封裝了在遍歷元素時刪除元素,removeIf(),以下:
list.removeIf(item -> "元素2".equals(item)
點進去能夠看到代碼和上一個差很少只是封裝了一層罷了。
default boolean removeIf(Predicate<? super E> filter) { Objects.requireNonNull(filter); boolean removed = false; final Iterator<E> each = iterator(); while (each.hasNext()) { if (filter.test(each.next())) { each.remove(); removed = true; } } return removed; }
1.咱們探究了好幾種遍歷時刪除元素的方式,也知道了fail-fast的基本概念。 2.學會了之後遍歷的時候刪除元素要使用迭代器哦,並且發現即便是單線程依然能夠拋出ConcurrentModificationException 3.若是是多線程操做list的話建議使用CopyOnWriteArrayList,或者對迭代器加鎖也行,看我的習慣吧~