使用 ConcurrentHashMap 進行多線程計數,8 個線程各自增 100 次,預期結果 800,實際運行多次結果均不等於 800。
ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>(); // 8 線程並發執行: if (!map.containsKey(KEY)) map.put(KEY, 1); else map.put(KEY, map.get(KEY) + 1); // 非原子操作!
單個方法調用是原子的(get、put、containsKey 等),但組合操作不是原子的。上述代碼包含三步:
map.get(KEY) - 讀取當前值+ 1 - 計算新值map.put(KEY, newValue) - 寫回線程 A 讀取時,線程 B 可能正在寫入,導致 A 讀到舊值,最終覆蓋 B 的更新(丟失更新)。
最初嘗試用 synchronized 修飾實例方法 addup(),但每個線程持有獨立的 MyTask 實例,鎖的是不同對象(this),無法互斥。
// 所有線程共享同一把鎖
private static final Object LOCK = new Object();
// 在 run() 中:
synchronized (LOCK) {
addup(); // 或直接在這裡寫邏輯
}
// computeIfAbsent + merge 組合實現原子自增 map.computeIfAbsent(KEY, k -> 0); map.merge(KEY, 1, Integer::sum); // 原子:old + 1 // 或更簡潔: map.compute(KEY, (k, v) -> v == null ? 1 : v + 1);
LongAdder counter = new LongAdder(); // 8 線程並發: counter.increment(); // 最後: map.put(KEY, counter.intValue());
public class Test40 {
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 100; ++i) {
System.out.println(test());
}
}
private static int test() throws InterruptedException {
// 方案三:LongAdder 最高效,最後轉 Map
Map<String, Integer> map = new HashMap<>();
LongAdder counter = new LongAdder();
ExecutorService pool = Executors.newCachedThreadPool();
for (int i = 0; i < 8; ++i) {
pool.execute(() -> {
for (int j = 0; j < 100; ++j) counter.increment();
});
}
pool.shutdown();
pool.awaitTermination(1, TimeUnit.DAYS);
map.put("key", counter.intValue());
return map.get("key");
}
}
| 概念 | 關鍵點 |
|---|---|
| ConcurrentHashMap 線程安全 | 單方法原子,復合操作非原子 |
| synchronized 鎖對象 | 必須是所有線程共享的對象 |
| 判斷同一對象 | 使用 == 比較引用地址 |
| 最佳計數器 | LongAdder (高並發) / AtomicLong (低並發) |
LongAdder,最後彙總到 Map