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

Java Fail-Fast 機制深度解析:原理、觸發條件與並發安全替代方案

     編輯:Java 相關知識

Fail-Fast(快速失敗) 是 Java 集合框架中一種錯誤檢測機制:當多線程並發修改集合結構時,迭代器能快速拋出 ConcurrentModificationException,而非產生不可預期的行為。本文以 ArrayList 為例,從源碼級原理、觸發場景、解決方案到並發包替代實現全維度剖析。

一、Fail-Fast 核心概念

  • 定義:迭代器遍歷過程中,檢測到集合結構被其他線程修改(add/remove/clear 等),立即拋出 ConcurrentModificationException。
  • 目的:檢測邏輯錯誤,非併發控制手段。JDK 文檔明確聲明:不保證一定觸發,不可依賴於程序正確性。
  • 適用集合:java.util 包下所有非線程安全集合(ArrayList、HashMap、LinkedList、Vector 等)。

二、觸發示例(雙線程競態)

import java.util.*;

public class FailFastDemo {
    static List<String> list = new ArrayList<>();  // 非線程安全

    public static void main(String[] args) {
        new Thread(() -> {
            for (int i = 0; i < 6; i++) {
                list.add(String.valueOf(i));
                printAll();
            }
        }, "Thread-A").start();

        new Thread(() -> {
            for (int i = 10; i < 16; i++) {
                list.add(String.valueOf(i));
                printAll();
            }
        }, "Thread-B").start();
    }

    static void printAll() {
        Iterator<String> it = list.iterator();
        while (it.hasNext()) System.out.print(it.next() + ", ");
        System.out.println();
    }
}
// 運行結果:拋出 java.util.ConcurrentModificationException

觸發條件拆解

  1. 線程 A 創建 Iterator,記錄 expectedModCount = modCount(假設為 N)。
  2. 線程 A 遍歷中,線程 B 執行 list.add() → modCount++ 變為 N+1。
  3. 線程 A 再次調用 it.next() → checkForComodification() 發現 expectedModCount != modCount → 拋異常。

三、ArrayList 源碼級原理剖析

3.1 modCount 與 expectedModCount

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

private class Itr implements Iterator<E> {
    int expectedModCount = modCount;   // 創建迭代器時快照

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

    public E next() {
        checkForComodification();      // 每次 next() 前檢查
        ...
    }
    public void remove() {
        checkForComodification();      // remove() 也檢查
        ...
        expectedModCount = modCount;   // 迭代器自身修改後同步快照
    }
}

3.2 修改 modCount 的操作

// ArrayList.java 片段
public boolean add(E e) {
    ensureCapacity(size + 1);  // 內部 modCount++
    elementData[size++] = e;
    return true;
}
public void add(int index, E element) {
    ensureCapacity(size + 1);  // modCount++
    System.arraycopy(...);
    elementData[index] = element;
    size++;
}
public E remove(int index) {
    modCount++;                // 直接修改
    ...
}
public void clear() {
    modCount++;
    ...
}

結論:任何導致元素數量變化的結構性修改(add/remove/clear 等)都會 modCount++。

四、單線程下的自觸發場景

List<String> list = new ArrayList<>(Arrays.asList("a", "b", "c"));
for (String s : list) {  // 隱式創建 Iterator
    if ("b".equals(s)) list.remove(s);  // 直接調用集合 remove → modCount++
}
// 拋 ConcurrentModificationException

正確做法:使用迭代器自身的 remove()。

Iterator<String> it = list.iterator();
while (it.hasNext()) {
    if ("b".equals(it.next())) it.remove();  // 迭代器 remove 會同步 expectedModCount
}

五、解決方案與並發安全替代

5.1 同步鎖(性能差、易死鎖)

synchronized (list) {
    for (String s : list) { ... }
}

缺點:遍歷期間鎖住整個集合,併發度極低。

5.2 複製快照遍歷(記憶體開銷大)

List<String> copy = new ArrayList<>(list);  // 複製快照
for (String s : copy) { ... }

5.3 並發包替代(推薦)

非線程安全集合並發包替代類特性
ArrayListCopyOnWriteArrayList寫時復制,遍歷無鎖,適合讀多寫少
HashMapConcurrentHashMapCAS + synchronized 細粒度鎖,高併發首選
LinkedListConcurrentLinkedDeque無鎖雙端隊列,適合高併發隊列/棧
Vector/Stack同上Vector 自帶同步但粗粒度,建議遷移

六、CopyOnWriteArrayList 如何避免 Fail-Fast

// CopyOnWriteArrayList.java 核心
public Iterator<E> iterator() {
    return new COWIterator<>(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++]; }
    public void remove() { throw new UnsupportedOperationException(); } // 迭代器不可修改
    // 無 modCount、無 checkForComodification、無 ConcurrentModificationException
}
核心思想:讀寫分離。寫入時復制新數組(Arrays.copyOf),讀取遍歷舊數組快照。迭代器見到的是創建時刻的靜態視圖,自然無併發修改衝突。

七、最佳實踐清單

  1. 單線程遍歷中修改 → 僅用 Iterator.remove() / ListIterator.add/set/remove。
  2. 多線程環境 → 優先選用 java.util.concurrent 包下對應並發集合。
  3. 讀多寫少、需遍歷一致性 → CopyOnWriteArrayList / CopyOnWriteArraySet。
  4. 高併發鍵值存儲 → ConcurrentHashMap。
  5. 高併發隊列/棧 → ConcurrentLinkedQueue / ConcurrentLinkedDeque / ArrayBlockingQueue。
  6. 不可依賴 Fail-Fast 保證正確性 → 它是檢測工具,非併發控制機制。
  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved