問題代碼回顧
public class JoinThread extends Thread {
public static volatile int n = 0;
public void run() {
for (int i = 0; i < 10; i++, n++) { // n++ 非原子操作!
try { sleep(2); } catch (Exception e) {}
}
}
public static void main(String[] args) throws Exception {
Thread[] threads = new Thread[100];
for (int i = 0; i < threads.length; i++) threads[i] = new JoinThread();
for (Thread t : threads) t.start();
for (Thread t : threads) t.join(); // 僅等待執行緒結束
System.out.println("n=" + JoinThread.n); // 預期 1000,實際 < 1000
}
}
錯誤根因
n++ 包含「讀取、遞增、寫回」三步,volatile 僅保證可見性,**不保證原子性**。
- 100 條執行緒並發執行
run(),極大概率發生「讀取舊值 → 遞增 → 覆寫」導致更新丟失。
join() 僅阻塞主執行緒直到目標執行緒結束,**無法修正已發生的資料競爭**。
修正方案對比
| 方案 | 程式碼片段 | 優點 | 缺點 |
| synchronized 方法 | public synchronized void run() { ... } | 簡單、鎖粒度大 | 並發度低,序列化執行 |
| synchronized 區塊 | synchronized(JoinThread.class) { n++; } | 鎖粒度較小 | 仍有鎖爭用 |
| AtomicInteger (推薦) | static AtomicInteger n = new AtomicInteger(); n.incrementAndGet(); | 無鎖、高效、語義清晰 | 僅支援基本原子操作 |
| LongAdder (高併發首選) | static LongAdder n = new LongAdder(); n.increment(); | 分段計數、極低爭用 | 最終一致性、Java 8+ |
最佳實踐版本
public class JoinThread extends Thread {
private static final LongAdder counter = new LongAdder();
@Override
public void run() {
for (int i = 0; i < 10; i++) {
counter.increment();
try { Thread.sleep(2); } catch (InterruptedException ignored) {}
}
}
public static void main(String[] args) throws InterruptedException {
Thread[] threads = new Thread[100];
for (int i = 0; i < threads.length; i++) threads[i] = new JoinThread();
for (Thread t : threads) t.start();
for (Thread t : threads) t.join();
System.out.println("n=" + counter.sum()); // 穩定輸出 1000
}
}
核心觀念總結
volatile ≠ 原子性;join() 只負責等待,不負責同步共享資料。
- 計數器、累加器等熱點共享變數,**優先選用
java.util.concurrent.atomic 包類別**。
- 若業務邏輯複雜需多變數一致性,再考慮
synchronized / ReentrantLock / STM。