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

Vector 與 Hashtable 線程安全陷阱:putAll 併發修改異常深度解析

     編輯:Java 相關知識

前言

Java 集合框架中,Vector 和 Hashtable 是早期 (JDK 1.0) 就存在的「線程安全」集合,其所有公開方法均以 synchronized 關鍵字修飾。然而,**單個方法的原子性不等於複合操作的線程安全**。本文透過一個經典的 putAll 併發修改異常案例,深度剖析其根因,並提供現代解決方案。

問題複現:ConcurrentModificationException

場景:一個執行緒持續向共享的 Hashtable 添加資料,另一個執行緒定期清空並複製該 Hashtable 的內容到本地 Map。

import java.util.*;

public class HashtableConcurrentIssue {

    // 共享資料源
    static class Constants {
        public static final Map USER_MAP = new Hashtable<>();
        public static final List USER_LIST = new Vector<>();
    }

    static class User {
        int id; String name;
        public User(int id, String name) { this.id = id; this.name = name; }
    }

    // 寫入執行緒:持續新增資料
    static class WriterThread extends Thread {
        @Override public void run() {
            for (int i = 0; i < 100_000; i++) {
                User u = new User(i, "name" + i);
                // Hashtable.put 是 synchronized,單一操作安全
                if (!Constants.USER_MAP.containsKey(i)) {
                    Constants.USER_MAP.put(i, u);
                    Constants.USER_LIST.add(u);
                }
            }
            System.out.println("寫入執行緒結束");
        }
    }

    public static void main(String[] args) {
        new WriterThread().start();

        Map localMap = new Hashtable<>();
        List localList = new Vector<>();

        for (int i = 0; i < 1000; i++) {
            localMap.clear();
            localList.clear();

            localList.addAll(Constants.USER_LIST); // Vector.addAll 安全

            // 關鍵問題點:Hashtable.putAll 非原子性複製
            localMap.putAll(Constants.USER_MAP); // 拋出 ConcurrentModificationException

            System.out.printf("迴圈 %d: Map大小=%d, List大小=%d%n", i, localMap.size(), localList.size());
        }
        System.out.println("主執行緒結束");
    }
}

錯誤堆疊追蹤

Exception in thread "main" java.util.ConcurrentModificationException
    at java.util.Hashtable$Enumerator.next(Hashtable.java:1378)
    at java.util.Hashtable.copyTo(Hashtable.java:1462)
    at java.util.Hashtable.putAll(Hashtable.java:1490)
    // ...

根因深度解析

1. Vector.addAll 為何安全?

查看 JDK 原始碼 (java.util.Vector):

public synchronized boolean addAll(Collection c) {
    // 關鍵:先將參數集合轉為陣列,再操作陣列
    Object[] a = c.toArray(); // 複製一份快照
    // ... 後續操作基於快照陣列 a
}

結論: Vector.addAll 內部首先調用 c.toArray() 產生一份**快照**,後續遍歷的是這份快照,不受原集合修改影響,因此不會拋出 CME。

2. Hashtable.putAll 為何不安全?

查看 JDK 原始碼 (java.util.Hashtable):

public synchronized void putAll(Map t) {
    // 直接獲取參數 Map 的 EntrySet 迭代器
    for (Map.Entry e : t.entrySet()) {
        put(e.getKey(), e.getValue()); // 直接遍歷原集合視圖
    }
}

結論: Hashtable.putAll **沒有複製快照**,直接遍歷傳入 Map 的 entrySet() 視圖。雖然 putAll 本身是 synchronized,鎖住的是 this (目標 Map),但**無法鎖住參數 Map (源 Map)**。當寫入執行緒修改源 Map 時,迭代器檢測到 modCount 變化,拋出 ConcurrentModificationException。

核心誤區:鎖的是誰?

  • synchronized 方法鎖定的是 **當前物件 (this)**。
  • 呼叫 target.putAll(source) 時,鎖住的是 target。
  • 源集合 source 並未被鎖定,寫入執行緒可自由修改它。

解決方案對比

方案代碼示例優點缺點
外部同步鎖源 Map
synchronized (Constants.USER_MAP) {
    localMap.putAll(Constants.USER_MAP);
}
最小改動,邏輯清晰擴展性差,易死鎖,鎖住源 Map 影響寫入效能
複製快照後再 putAll
Map snapshot = new HashMap<>(Constants.USER_MAP);
localMap.putAll(snapshot);
無鎖,效能好,源 Map 可繼續寫入快照瞬間佔用雙倍記憶體
使用 ConcurrentHashMap (推薦)
static final Map USER_MAP = new ConcurrentHashMap<>();
// putAll 內部採用分段鎖/CAS,支援併發遍歷
localMap.putAll(USER_MAP);
高併發效能極佳,API 豐富 (computeIfAbsent 等)弱一致性迭代器,遍歷時可能不反映最新狀態
Collections.synchronizedMap
Map syncMap = Collections.synchronizedMap(new HashMap<>());
// 仍需手動同步遍歷/putAll
synchronized(syncMap) { local.putAll(syncMap); }
包裝現有 Map本質同方案一,複合操作仍需外部同步

最佳實踐建議

  1. 棄用 Vector/Hashtable:它們是舊版遺留類,效能低、擴展性差。
  2. 首選 ConcurrentHashMap:JDK 8+ 採用 CAS + synchronized + 紅黑樹,效能遠超 Hashtable。
  3. 需要一致性快照時:手動 new HashMap<>(concurrentMap) 複製一份,再對副本操作。
  4. 複合操作必須外部同步:即使使用線程安全集合,if (!map.containsKey(k)) map.put(k, v) 這類「檢查-執行」模式仍需額外加鎖或使用原子方法 (putIfAbsent)。

總結

Vector 和 Hashtable 的「線程安全」僅限於**單個方法調用**。putAll 因直接遍歷源集合視圖而非複製快照,導致在併發修改源集合時拋出 ConcurrentModificationException。現代 Java 開發應全面遷移至 ConcurrentHashMap、CopyOnWriteArrayList 等 `java.util.concurrent` 包下的高併發集合,並理解**一致性快照**與**弱一致性迭代器**的權衡。

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