synchronized 這東西從大學開始就在教,但會用和懂用是兩碼子事。踩過不少坑之後,發現這六種同步模式各有眉角,搞不清楚一定會爆。

模式 1:Synchronized Method

最簡單的寫法,直接加在方法上:

1
2
3
4
5
6
7
8
9
10
11
public class Counter {
private int count = 0;

public synchronized void increment() {
count++;
}

public synchronized int getCount() {
return count;
}
}

鎖住的對象是什麼? this(呼叫這個方法的物件實例)

1
2
3
4
5
6
7
8
9
10
Counter counter1 = new Counter();
Counter counter2 = new Counter();

Thread t1 = new Thread(counter1::increment);
Thread t2 = new Thread(counter1::increment);
Thread t3 = new Thread(counter2::increment);

t1.start(); // 被鎖住
t2.start(); // 等 t1 完成,被鎖住
t3.start(); // 不被鎖,counter2 是另一個物件

優點: 簡單直觀
缺點: 粒度太大,整個方法都鎖,效能不好

模式 2:Synchronized Static Method

加在 static 方法上:

1
2
3
4
5
6
7
public class GlobalCounter {
private static int count = 0;

public static synchronized void increment() {
count++;
}
}

鎖住的對象是什麼? Class 物件(GlobalCounter.class)

1
2
3
// 所有執行緒都會因為同一個 Class 物件而被鎖住
GlobalCounter.increment(); // 執行緒 A
GlobalCounter.increment(); // 執行緒 B - 等 A 完成

重點: 即使有多個 GlobalCounter 實例,也只有一個 Class 物件,所以都會被鎖住。

模式 3:Synchronized Block - this

把鎖縮小到特定的 code block,鎖 this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class Account {
private double balance = 0;

public void withdraw(double amount) {
// 不需要鎖的操作(比如 log)
System.out.println("Withdrawing " + amount);

// 只鎖需要同步的部分
synchronized(this) {
if (balance >= amount) {
balance -= amount;
}
}

// 提款後的操作(比如通知)
System.out.println("Withdrawal complete");
}
}

優點: 粒度更小,只鎖臨界區域(critical section)
缺點: 要小心嵌套和死結

模式 4:Synchronized Block - Object

鎖住指定的物件而不是 this。這招超強,能細粒度控制多個資源:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class BankTransfer {
private Object lock1 = new Object();
private Object lock2 = new Object();

private Account accountA;
private Account accountB;

public void transfer(double amount) {
// 轉帳涉及兩個帳戶,分別用兩個 lock
synchronized(lock1) {
accountA.withdraw(amount);
}

synchronized(lock2) {
accountB.deposit(amount);
}
}
}

或者更常見的,用物件本身當 lock:

1
2
3
4
5
6
7
8
9
10
public class OrderService {
private Map<Long, Order> orders = new HashMap<>();
private Object lockObj = new Object();

public void updateOrder(Long orderId, Order newOrder) {
synchronized(lockObj) {
orders.put(orderId, newOrder);
}
}
}

優點: 彈性大,能針對不同資源用不同 lock
缺點: 複雜度提升,容易 deadlock

模式 5:Synchronized Block - Class

鎖 Class 物件,跟 static synchronized method 同效果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class GlobalCache {
private static Map<String, Object> cache = new HashMap<>();

public static void put(String key, Object value) {
synchronized(GlobalCache.class) {
cache.put(key, value);
}
}

public static Object get(String key) {
synchronized(GlobalCache.class) {
return cache.get(key);
}
}
}

特點: 所有執行緒都會被同一個 Class lock 鎖住

模式 6:Double-Checked Locking + volatile

這招是為了優化性能,避免每次都進入 synchronized block:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class Singleton {
private static volatile Singleton instance = null;

public static Singleton getInstance() {
// 第一次檢查(沒有 lock,快)
if (instance == null) {
// 第二次檢查(有 lock,確保執行緒安全)
synchronized(Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}

為什麼需要 volatile?

1
2
3
4
5
6
// 沒有 volatile 的危險做法
private static Singleton instance = null;

// 執行緒 A:進入 synchronized,執行 new Singleton()
// 執行緒 B:可能看到 instance 已經被賦值(但物件還沒初始化完)
// 導致 B 直接用了未初始化的 instance

volatile 確保對 instance 的寫入對所有執行緒可見。

何時用? 只在 Singleton 或類似的「初始化一次」場景。其他地方別瞎用。

六種模式的對比表

模式 鎖對象 粒度 效能 複雜度
synchronized method this 大(整個方法) 差 低
synchronized static method Class 大(整個方法) 差 低
synchronized(this) this 小(code block) 好 中
synchronized(object) 任意物件 小 好 高
synchronized(ClassName.class) Class 中等 中等 低
Double-checked locking Class 很小(只初始化時 lock) 很好 高

實務建議

優先順序

  1. 能不用就不用 - 用 volatile、atomic class、ConcurrentHashMap 等
  2. 用 block 不用 method - 粒度越小效能越好
  3. 鎖最少的資源 - 不要把 synchronized 擴得太大
  4. 避免嵌套 - 複數 lock 容易 deadlock

常見的坑

坑 1:internal call 不會 lock

1
2
3
4
5
6
7
8
9
10
public class Service {
public synchronized void methodA() {
methodB(); // 這邊的呼叫是在 lock 內沒錯
}

public synchronized void methodB() {
// 但同執行緒呼叫時,同一個 this lock 能重複進入
// 這叫 reentrant lock
}
}

synchronized 是 reentrant 的,同一個執行緒能重複進入同一個 lock。

坑 2:HashMap + synchronized block 還是線程不安全

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 這樣寫還是有 race condition
private HashMap map = new HashMap();

public void put(String key, Object value) {
synchronized(map) {
map.put(key, value);
}
}

public boolean contains(String key) {
synchronized(map) {
return map.containsKey(key);
}
}

// 使用端的問題
if (map.contains("key")) { // 檢查
String value = map.get("key"); // 取值 - 中間可能被別人刪掉
}

要改成:

1
2
3
4
5
synchronized(map) {
if (map.contains("key")) {
String value = map.get("key");
}
}

坑 3:wait/notify 用法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 錯誤 - wait() 要在 synchronized 內
public void waitForSignal() {
this.wait(); // 拋 IllegalMonitorStateException
}

// 正確
public synchronized void waitForSignal() {
this.wait();
}

// 或
public void waitForSignal() {
synchronized(this) {
this.wait();
}
}

什麼時候該考慮 ReentrantLock

synchronized 滿足 90% 的場景,但有些情況該用 ReentrantLock:

  1. 需要 fair lock(公平性)
  2. 需要超時設定
  3. 需要 lock 多個條件變數
  4. 需要中斷執行緒
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class FairLockExample {
private final ReentrantLock lock = new ReentrantLock(true); // fair mode

public void doSomething() throws InterruptedException {
if (lock.tryLock(5, TimeUnit.SECONDS)) {
try {
// 做事
} finally {
lock.unlock();
}
} else {
System.out.println("Failed to acquire lock");
}
}
}

但對於簡單的同步,synchronized 還是第一選擇。

最後的話

synchronized 看似簡單,其實坑超多。能用 block 就不要用 method,能用 ConcurrentHashMap 就不要自己加 synchronized。記住六種模式各自的特點,選對工具,才不會寫出有 race condition 的爛程式。