作為 Java Web 開發者,容器(如 Tomcat)已幫我們處理了大部分線程管理工作,但這不代表我們可以忽視線程安全的基本原理。許多開發者在 Java SE 基礎未紮實前就急著轉向 Java EE,導致知識體系「博而不精」。本文通過一個經典的線程不安全案例,剖析問題根源並提供多種解決思路。
定義一個帶有實例變數的計數器類:
public class Count {
private int num; // 共享變數
public void count() {
for (int i = 1; i <= 10; i++) {
num += i; // 非原子操作:讀取、計算、寫回
}
System.out.println(Thread.currentThread().getName() + "-" + num);
}
}
啟動 10 個線程共享同一個 Count 實例:
public class ThreadTest {
public static void main(String[] args) {
Runnable runnable = new Runnable() {
Count count = new Count(); // 所有線程共享同一實例
public void run() {
count.count();
}
};
for (int i = 0; i < 10; i++) {
new Thread(runnable).start();
}
}
}
Thread-0-55
Thread-1-110
Thread-2-165
Thread-3-220
Thread-4-275
Thread-5-330
Thread-6-385
Thread-7-440
Thread-8-495
Thread-9-550
預期每個線程輸出 55(1+2+...+10),實際卻呈現遞增趨勢。原因在於 num += i 不是原子操作,多線程並發讀寫導致數據競爭,後啟動的線程讀取到已被修改的 num 值。
public class Count {
public void count() {
int num = 0; // 每次調用創建新的局部變數,線程隔離
for (int i = 1; i <= 10; i++) {
num += i;
}
System.out.println(Thread.currentThread().getName() + "-" + num);
}
}
原理:局部變數存放在線程私有的棧幀中,天然線程安全,無同步開銷,效能最佳。
public class ThreadTest4 {
public static void main(String[] args) {
Runnable runnable = new Runnable() {
public void run() {
Count count = new Count(); // 每個線程創建自己的實例
count.count();
}
};
for (int i = 0; i < 10; i++) {
new Thread(runnable).start();
}
}
}
原理:將共享狀態轉為線程封閉狀態,每個線程操作自己的對象副本,避免競爭。
雖然可行,但會產生大量對象,增加 GC 壓力,且代碼冗餘。
當無法使用局部變數或線程封閉時,需引入同步機制:synchronized、Lock、ThreadLocal、原子類(AtomicInteger)或並發集合。選擇原則:優先無共享狀態設計,次之無鎖並發,最後才是鎖優化。