ConcurrentModificationException 是 Java 開發者最熟悉的「老朋友」。本文從 ArrayList 源碼層面徹底講透 Fail-Fast 觸發條件、modCount 機制、迭代器檢查邏輯,並對比 CopyOnWriteArrayList 等安全失敗容器的設計權衡,幫你在並發場景做出正確選型。
定義:Java 集合的一種錯誤檢測機制。當多線程對集合進行操作時,若某線程通過 Iterator 遍歷集合,而另一線程結構性修改了集合(增/刪/擴容),則拋出 ConcurrentModificationException,快速失敗,避免產生不確定結果。
add/remove/clear)。非結構性:僅修改元素內部屬性(list.get(i).setName("new"))。java.util 所有集合(ArrayList/HashMap/HashSet...)均為 Fail-Fast;java.util.concurrent 集合為 Fail-Safe(安全失敗)。// AbstractList.java
protected transient int modCount = 0; // 修改次數計數器
每次結構性修改(add/remove/clear/ensureCapacity)均 modCount++。
// AbstractList.Itr
int expectedModCount = modCount; // 創建迭代器時拍攝快照
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
// next()、remove() 等關鍵方法均調用此檢查
public E next() {
checkForComodification();
// ...
}
expectedModCount = modCount (假設 = 5)list.add() → modCount++ (變 6)iterator.next() → checkForComodification() 發現 5 != 6 → 拋出異常。List<String> list = new ArrayList<>();
list.add("A"); list.add("B");
new Thread(() -> {
for (String s : list) { // 隱式創建 Iterator
System.out.println("Thread-1 讀取: " + s);
try { Thread.sleep(100); } catch (InterruptedException ignored) {}
}
}).start();
new Thread(() -> {
try { Thread.sleep(50); } catch (InterruptedException ignored) {}
list.add("C"); // 結構性修改
System.out.println("Thread-2 添加 C");
}).start();
// 輸出:Thread-1 讀取 A → Thread-2 添加 C → Thread-1 次次調用 next() → 拋 ConcurrentModificationException
| 方案 | 核心思想 | 適用場景 | 優點 | 缺點 |
|---|---|---|---|---|
| Collections.synchronizedList | 全量鎖 | 低併發、讀寫均衡 | 簡單、API 兼容 | 鎖粒度大、迭代需手動同步、擴容仍可能拋異常 |
| CopyOnWriteArrayList | 寫時復制、讀不加鎖 | 讀多寫少、遍歷頻繁、需弱一致性 | 讀極快、無 Fail-Fast、迭代器穩定 | 寫入慢、內存佔用大、數據最終一致 |
| 並發佇列 | CAS/鎖分段 | 生產者消費者、任務調度 | 高吞吐、天生線程安全 | 不支持隨機訪問、無索引 |
| 手動同步迭代 | synchronized(list) { for(...) } | 臨時、低頻 | 零依賴 | 易死鎖、鎖住整個集合、擴容期間仍阻塞 |
// 無 modCount、無繼承 AbstractList
private transient volatile Object[] array; // 數組引用 volatile 保證可見性
final transient ReentrantLock lock = new ReentrantLock(); // 寫鎖
public boolean add(E e) {
final ReentrantLock lock = this.lock;
lock.lock();
try {
Object[] elements = getArray();
int len = elements.length;
Object[] newElements = Arrays.copyOf(elements, len + 1);
newElements[len] = e;
setArray(newElements); // volatile 寫,對其他線程可見
return true;
} finally {
lock.unlock();
}
}
public Iterator<E> iterator() {
return new COWIterator<E>(getArray(), 0); // 傳入當前數組引用
}
static final class COWIterator<E> implements ListIterator<E> {
private final Object[] snapshot; // 快照,不隨原數組變化
private int cursor;
COWIterator(Object[] elements, int initialCursor) {
this.snapshot = elements; // 關鍵:持有舊數組引用
this.cursor = initialCursor;
}
public boolean hasNext() { return cursor < snapshot.length; }
public E next() { return (E) snapshot[cursor++]; }
// remove/set/add 直接拋 UnsupportedOperationException
}
get/iterator/forEach 直接訪問 volatile array,極高併發讀性能。Arrays.copyOf,O(n) 內存拷貝 + 鎖競爭。
需要並發安全的 List?
│
├─ 讀多寫少(> 95% 讀)、頻繁遍歷、可接受弱一致性 → CopyOnWriteArrayList
│
├─ 寫入頻繁、需強一致性、隨機訪問 → Collections.synchronizedList / 手動同步 / 遷移到 ConcurrentHashMap (Key=Index)
│
├─ 生產者消費者、任務隊列 → BlockingQueue (ArrayBlockingQueue/LinkedBlockingQueue)
│
└─ 高併發計數器、ID 生成器 → LongAdder / AtomicLong
synchronized(list) { for(...)} 只有迭代期間加鎖,若其他代碼未加同步鎖修改,仍會觸發。Fail-Fast 通過 modCount 快照機制實現「快速發現併發修改 Bug」。ArrayList 非線程安全,並發場景必須選型:讀多寫少用 CopyOnWriteArrayList,寫多用同步包裝或並發佇列,極致性能上 Disruptor/自定義數據結構。理解底層 volatile array + ReentrantLock + Arrays.copyOf 的權衡,才能駕馭並發集合。