同步容器(Vector、Hashtable、Collections.synchronizedXxx 包裝類)雖然單個方法是線程安全的,但在復合操作場景下仍存在競態條件風險。本文深入剖析「檢查-然後執行」模式的併發缺陷,以及隱藏迭代器導致的 ConcurrentModificationException 異常。
以 Vector 為例,「獲取最後一個元素」與「刪除最後一個元素」是兩個獨立的同步方法。若線程 A 正在 get(lastIndex),線程 B 同時 remove(lastIndex),可能導致 A 拋出 ArrayIndexOutOfBoundsException。
// 線程 A Object last = vector.get(vector.size() - 1); // 線程 B vector.remove(vector.size() - 1); // 可能導致線程 A 索引越界
synchronized (vector) {
Object last = vector.get(vector.size() - 1);
// 或刪除操作
vector.remove(vector.size() - 1);
}
將復合操作封裝在同一把鎖(容器對象自身)中,使其成為原子操作。
看似線程安全的類內部若在 toString()、hashCode()、equals()、日誌輸出等場景隱式調用迭代器,而在迭代過程中其他線程修改了容器,即拋出 ConcurrentModificationException。
public class HiddenIterator {
private final Set<Integer> set = new HashSet<>();
public synchronized void add(Integer i) { set.add(i); }
public synchronized void remove(Integer i) { set.remove(i); }
public void addTenThings() {
Random r = new Random();
for (int i = 0; i < 10; i++) add(r.nextInt());
// 危險!toString() 內部會迭代 set
// 若此時其他線程調用 add/remove,拋出 CME
System.out.println("DEBUG: added ten elements to " + set);
}
}
public void addTenThings() {
Random r = new Random();
for (int i = 0; i < 10; i++) add(r.nextInt());
synchronized (this) { // 持有對象鎖,防止併發修改
System.out.println("DEBUG: added ten elements to " + set);
}
}
ConcurrentHashMap、CopyOnWriteArrayList、ConcurrentLinkedQueue 等提供更高併發性能與弱一致性迭代器,避免 CME。List.of()、Set.of()、Guava ImmutableList 等天然線程安全,適合只讀場景。同步容器僅保證單方法原子性,復合操作與隱式迭代是兩大併發陷阱。實戰中應優先選用 java.util.concurrent 並發容器,或顯式加鎖保護臨界區。