多線程讓程式能「一心多用」,但也引入了經典的線程安全問題。當多個線程同時讀寫同一份資料(如變數、檔案、資料庫連線)時,若缺乏協調機制,就會產生「髒讀」、「丟失更新」等難以復現的錯誤。這類被多線程共同存取的資源,稱為臨界資源。
我們建立一個 Data 類封裝 int n = 60,兩個 DataThread 共享同一個 Data 實例,各自循環 10 次:印出數值 → 遞減 1 → 睡 200ms。
public class Data { public int n = 60; }
public class DataThread extends Thread {
private Data data; private String name;
public DataThread(Data data, String name) { this.data=data; this.name=name; start(); }
public void run() {
for (int i=0;i<10;i++) {
System.out.println(name + ":" + data.n);
data.n--;
Thread.sleep(200);
}
}
}
線程1:60
線程2:60 ← 重複讀到 60
線程2:58
線程1:58 ← 重複讀到 58
...
線程2:47 ← 甚至出現中間值 47
原因:線程 1 讀取 n=60 尚未印出/遞減,CPU 切換到線程 2,線程 2 也讀到 n=60 並遞減為 59,線程 1 恢復執行遞減為 58……操作被打斷、交錯,導致邏輯錯誤。
帳戶餘額 1000 元,存折與 ATM 卡同時取 800 元。若無同步,兩邊都讀到 1000 → 都判定可取 → 都扣 800 → 最終餘額 -600,銀行虧損。這就是典型的競態條件。
Java 提供 synchronized 關鍵字,為方法或程式碼區塊加鎖。規則簡單:**同一個物件實例上,同一時間只允許一個執行緒進入被 synchronized 修飾的區域**。
public class Data2 {
public int n = 60;
// 整個方法加鎖,確保「讀-改-寫」不可分割
public synchronized void action(String name) {
System.out.println(name + ":" + n);
n--;
}
}
public class Data2Thread extends Thread {
private Data2 data; private String name;
public Data2Thread(Data2 data, String name) { this.data=data; this.name=name; start(); }
public void run() {
for (int i=0;i<10;i++) {
data.action(name); // 呼叫同步方法
Thread.sleep(200);
}
}
}
線程1:60
線程2:59
線程2:58
線程1:57
... ← 每個數值只被處理一次,嚴格遞減
synchronized 修飾實例方法 → 鎖是 this(當前物件實例)。synchronized 修飾靜態方法 → 鎖是 ClassName.class(類物件)。synchronized(obj) { ... } 同步區塊 → 鎖是 obj(需為同一實例)。⚠️ 關鍵:不同實例互不干擾。只有「多線程操作同一個物件實例」時,鎖才生效。
櫃員 = 共享物件,客戶 = 線程。
客戶 A 正在辦理業務(持有鎖),客戶 B 只能排隊等待(阻塞)。即使 A 中途接電話(進入阻塞態),B 依然不能插隊——這就是synchronized的「獨佔性」。
synchronized 是解決線程安全最基礎、最直接的工具,適用於「單純互斥存取」場景。但面對「條件等待、生產消費協作」等複雜場景時,單純互斥會造成資源浪費(忙等待)。下一篇將介紹 同步機制——透過 wait()/notify() 實現線程間的高效協作。