首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> Java 程式設計 >> Java 相關知識

Java Fail-Fast 機制深度解析:從 ArrayList 源碼到 CopyOnWriteArrayList 解決方案

     編輯:Java 相關知識

ConcurrentModificationException 是 Java 開發者最熟悉的「老朋友」。本文從 ArrayList 源碼層面徹底講透 Fail-Fast 觸發條件、modCount 機制、迭代器檢查邏輯,並對比 CopyOnWriteArrayList 等安全失敗容器的設計權衡,幫你在並發場景做出正確選型。

一、什麼是 Fail-Fast?

定義:Java 集合的一種錯誤檢測機制。當多線程對集合進行操作時,若某線程通過 Iterator 遍歷集合,而另一線程結構性修改了集合(增/刪/擴容),則拋出 ConcurrentModificationException,快速失敗,避免產生不確定結果。

  • 結構性修改:改變集合大小、元素順序(add/remove/clear)。非結構性:僅修改元素內部屬性(list.get(i).setName("new"))。
  • 不保證必然拋出:JDK 文檔明確指出,Fail-Fast 為「最佳努力」,僅用於檢測 Bug,不能依賴它做並發控制。
  • 範圍:java.util 所有集合(ArrayList/HashMap/HashSet...)均為 Fail-Fast;java.util.concurrent 集合為 Fail-Safe(安全失敗)。

二、ArrayList 源碼級原理剖析

1. 核心字段:modCount

// AbstractList.java
protected transient int modCount = 0; // 修改次數計數器

每次結構性修改(add/remove/clear/ensureCapacity)均 modCount++。

2. 迭代器快照:expectedModCount

// AbstractList.Itr
int expectedModCount = modCount; // 創建迭代器時拍攝快照

3. 核心檢查:checkForComodification()

final void checkForComodification() {
    if (modCount != expectedModCount)
        throw new ConcurrentModificationException();
}

// next()、remove() 等關鍵方法均調用此檢查
public E next() {
    checkForComodification();
    // ...
}

4. 觸發完整鏈路

  1. 線程 A 創建 Iterator:expectedModCount = modCount (假設 = 5)
  2. 線程 B 執行 list.add() → modCount++ (變 6)
  3. 線程 A 呼叫 iterator.next() → checkForComodification() 發現 5 != 6 → 拋出異常。

三、Fail-Fast 示例代碼

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(...) }臨時、低頻零依賴易死鎖、鎖住整個集合、擴容期間仍阻塞

五、CopyOnWriteArrayList 原理深度解析

1. 核心結構

// 無 modCount、無繼承 AbstractList
private transient volatile Object[] array; // 數組引用 volatile 保證可見性
final transient ReentrantLock lock = new ReentrantLock(); // 寫鎖

2. 寫入:加鎖 + 複製數組 + 替換引用

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();
    }
}

3. 讀取/迭代:直接使用當前數組快照

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
}

4. 關鍵特性

  • 讀不加鎖:get/iterator/forEach 直接訪問 volatile array,極高併發讀性能。
  • 迭代器穩定:遍歷的是創建迭代器時的數組快照,後續寫入不影響當前遍歷(弱一致性)。
  • 寫入代價大:每次增刪改都 Arrays.copyOf,O(n) 內存拷貝 + 鎖競爭。
  • 內存佔用雙倍:寫入瞬間新舊數組共存,大列表慎用。

六、選型決策樹


需要並發安全的 List?
  │
  ├─ 讀多寫少(> 95% 讀)、頻繁遍歷、可接受弱一致性 → CopyOnWriteArrayList
  │
  ├─ 寫入頻繁、需強一致性、隨機訪問 → Collections.synchronizedList / 手動同步 / 遷移到 ConcurrentHashMap (Key=Index)
  │
  ├─ 生產者消費者、任務隊列 → BlockingQueue (ArrayBlockingQueue/LinkedBlockingQueue)
  │
  └─ 高併發計數器、ID 生成器 → LongAdder / AtomicLong

七、常見誤區

  1. 以為 Fail-Fast 是線程安全機制:它是檢測機制,而非防護機制。拋異常時數據可能已損壞。
  2. 在同步塊中迭代以為安全:synchronized(list) { for(...)} 只有迭代期間加鎖,若其他代碼未加同步鎖修改,仍會觸發。
  3. CopyOnWriteArrayList 適合所有場景:寫入放大效應嚴重,百萬級列表每秒寫入百次會導致 GC 暴增、延遲飆升。
  4. 忽略弱一致性語義:遍歷過程中新增元素不會被看到,已刪除元素仍會被遍歷到(若在快照中)。

八、總結

Fail-Fast 通過 modCount 快照機制實現「快速發現併發修改 Bug」。ArrayList 非線程安全,並發場景必須選型:讀多寫少用 CopyOnWriteArrayList,寫多用同步包裝或並發佇列,極致性能上 Disruptor/自定義數據結構。理解底層 volatile array + ReentrantLock + Arrays.copyOf 的權衡,才能駕馭並發集合。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved