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

線程安全遍歷 List 完全指南:Vector 加鎖 vs CopyOnWriteArrayList 寫時復制

     編輯:Java 綜合教學

五種遍歷方式回顧

// 1. 索引遍歷(不推薦,LinkedList 性能差)
for (int i = 0; i < list.size(); i++) { System.out.println(list.get(i)); }

// 2. Iterator 顯式遍歷
Iterator it = list.iterator();
while (it.hasNext()) { System.out.println(it.next()); }

// 3. 增強 for 循環(本質同 Iterator)
for (Object item : list) { System.out.println(item); }

// 4. Java 8 forEach + 匿名類
list.forEach(new Consumer() {
    @Override public void accept(Object item) { System.out.println(item); }
});

// 5. Java 8 forEach + Lambda(推薦)
list.forEach(item -> System.out.println(item));

方式 2/3 本質相同;方式 4/5 本質相同。

並發修改導致的 ConcurrentModificationException

List list = new ArrayList<>();
// 初始化 0-4
new Thread(() -> {  // 遍歷線程
    for (int item : list) {
        System.out.println("遍歷: " + item);
        Thread.sleep(1000);
    }
}).start();

new Thread(() -> {  // 修改線程
    Thread.sleep(1000);
    list.remove(4);
    System.out.println("list.remove(4)");
}).start();

輸出:

遍歷: 0
遍歷: 1
list.remove(4)
Exception in thread "Thread-0" java.util.ConcurrentModificationException

原因:遍歷時內部維護 modCount,修改操作導致 expectedModCount != modCount,拋出異常(fail-fast)。

方案一:Vector + 手動 synchronized 鎖

Vector 單個方法同步,但無法鎖住整個遍歷過程。需客戶端加鎖:

List list = new Vector<>();
// 遍歷線程
synchronized (list) {
    for (int item : list) { ... }
}
// 修改線程無需加鎖,Vector 內部方法已同步,會等待鎖釋放

結果: 遍歷完成後才執行 remove,輸出 0-4 再打印 remove。

缺點: 遍歷期間阻塞所有寫操作,並發性極差;若遍歷耗時長,寫線程饑餓。

方案二:CopyOnWriteArrayList(推薦讀多寫少)

核心原理:寫時復制。寫操作(add/remove/set)會複製底層數組,修改副本,最後原子切換引用。遍歷時讀取的是快照,不受後續修改影響。

List list = new CopyOnWriteArrayList<>();
// 遍歷線程(無需加鎖)
for (int item : list) { ... }
// 修改線程
list.remove(4);

結果:

遍歷: 0
遍歷: 1
list.remove(4)
遍歷: 2
遍歷: 3
遍歷: 4   // 仍遍歷到已刪除的 4,因讀取的是舊快照

特點:

  • 遍歷線程安全、無鎖、高並發讀性能極佳
  • 寫入代價大(複製數組),適合「讀多寫少、容忍短暫不一致」場景
  • 迭代器不支持 remove()、add() 等修改操作(拋 UnsupportedOperationException)

方案三:Vector.forEach()(Java 8+ 隱式同步)

Vector 重寫了 forEach 方法並加上 synchronized:

public synchronized void forEach(Consumer action) { ... }

測試證明:list.forEach(item -> ...) 遍歷期間,remove 會等待遍歷完成。

List list = new Vector<>();
list.forEach(item -> {  // 線程安全遍歷
    System.out.println(item);
    Thread.sleep(1000);
});
// 另一線程 list.remove(4) 將阻塞至遍歷結束

優勢: 代碼簡潔,無需顯式 synchronized 塊。

注意: ArrayList、CopyOnWriteArrayList 的 forEach 未加同步,非線程安全。

選型決策表

場景推薦方案核心理由
讀寫均衡、需強一致性Vector + synchronized 塊 / Collections.synchronizedList手動控制鎖範圍,保證遍歷與修改原子性
極度讀多寫少、容忍弱一致CopyOnWriteArrayList遍歷零鎖、高吞吐,寫入複製數組代價大但頻率低
代碼簡潔、Java 8+、Vector 既有代碼Vector.forEach()內部 synchronized,一行代碼實現線程安全遍歷
高並發 Map/Set 遍歷ConcurrentHashMap / ConcurrentSkipListMap弱一致性迭代器,無需加鎖,性能最優

總結

  • fail-fast 是保護機制,非 Bug,提醒你「遍歷期間被併發修改」
  • 理解 寫時復制 與 客戶端加鎖 兩種哲學差異:前者空間換時間(讀快),後者時間換安全(寫等)
  • Java 8 forEach 只是語法糖,線程安全性取決於具體集合實現,切勿想當然
  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved