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 的爛程式。