Java 集合框架提供了兩代線程安全容器:早期的同步容器(Vector、Hashtable、Collections.synchronizedXxx)與 JDK 5+ 引入的並發容器(ConcurrentHashMap、CopyOnWriteArrayList、SynchronousQueue 等)。本文深度對比兩代設計哲學,並重點剖析 ConcurrentHashMap 與 SynchronousQueue 核心實現。
| 維度 | 同步容器 | 並發容器 |
|---|---|---|
| 代表類 | Vector, Hashtable, SynchronizedList/Map/Set | ConcurrentHashMap, CopyOnWriteArrayList, ConcurrentLinkedQueue, SynchronousQueue |
| 實現方式 | 方法級 synchronized,鎖住整個對象 | 細粒度鎖、CAS、複製於寫、無鎖算法 |
| 並發性能 | 所有操作串行,高競爭時嚴重爭用 | 支持高並發讀寫,吞吐量隨 CPU 核心線性擴展 |
| 復合操作 | 需額外客戶端加鎖(如「檢查後獲取」) | 內建原子復合操作(putIfAbsent、remove(key, value)) |
| 迭代器 | 快速失效,需加鎖遍歷 | 弱一致性迭代器,無需加鎖,遍歷時可見部分修改 |
| 擴展性 | 差 | 優 |
// 看似安全,實則有競態條件
public static Object getLast(Vector list) {
int idx = list.size() - 1; // 線程 A 讀到 size=10
return list.get(idx); // 線程 B 先 remove(9),A 再 get(9) → IndexOutOfBounds
}
public static void deleteLast(Vector list) {
list.remove(list.size() - 1);
}
解決:客戶端加鎖 synchronized(list) { ... },但破壞封裝、易死鎖。並發容器內建原子方法避免此問題。
get() 通過 volatile Node 數組 + final 域實現無鎖讀取。put()、remove() 僅鎖定目標段,16 段理論支持 16 線程並發寫入。刪除節點 C 時,不銷毀原鏈表,而是克隆前驅節點 A→B 組成新鏈表,C 及其後節點保持不變:
原鏈表:A → B → C → D → E 刪除 C 後:新鏈表 A' → B' → D → E (C 仍指向 D,舊讀線程繼續走 A→B→C→D…)
效果:
ConcurrentModificationException。putIfAbsent、computeIfAbsent、merge 等。isEmpty() 永遠 true,remainingCapacity() 永遠 0。put(e) 必須等待消費者 take() 到來,反之亦然——雙方會面即交換。iterator() 返回空,peek() 返回 null。| 模式 | 構造方式 | 底層結構 | 適用場景 |
|---|---|---|---|
| 非公平(默認) | new SynchronousQueue() | LIFO 棧 + 非公平鎖 | 高吞吐,但可能導致線程飢餓 |
| 公平 | new SynchronousQueue(true) | FIFO 隊列 + 公平鎖 | 嚴格先來先服務,延遲略高 |
Executors.newCachedThreadPool()
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>()); // 任務直接移交給空閒線程,無隊列積壓
}
新任務到達 → 若有空閒線程直接移交執行 → 否則創建新線程 → 60s 空閒回收。適合大量短生命週期任務。
ConcurrentHashMap。CopyOnWriteArrayList/Set(寫入複製數組,讀無鎖)。SynchronousQueue。ArrayBlockingQueue(公平/非公平)、LinkedBlockingQueue(無界/有界)。DelayQueue、PriorityBlockingQueue。