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(); t3.start();
|
優點: 簡單直觀
缺點: 粒度太大,整個方法都鎖,效能不好
模式 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
| GlobalCounter.increment(); GlobalCounter.increment();
|
重點: 即使有多個 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) { 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) { 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() { if (instance == null) { synchronized(Singleton.class) { if (instance == null) { instance = new Singleton(); } } } return instance; } }
|
為什麼需要 volatile?
1 2 3 4 5 6
| private static Singleton instance = null;
|
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) |
很好 |
高 |
實務建議
優先順序
- 能不用就不用 - 用 volatile、atomic class、ConcurrentHashMap 等
- 用 block 不用 method - 粒度越小效能越好
- 鎖最少的資源 - 不要把 synchronized 擴得太大
- 避免嵌套 - 複數 lock 容易 deadlock
常見的坑
坑 1:internal call 不會 lock
1 2 3 4 5 6 7 8 9 10
| public class Service { public synchronized void methodA() { methodB(); } public synchronized void methodB() { } }
|
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
| 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
| public void waitForSignal() { this.wait(); }
public synchronized void waitForSignal() { this.wait(); }
public void waitForSignal() { synchronized(this) { this.wait(); } }
|
什麼時候該考慮 ReentrantLock
synchronized 滿足 90% 的場景,但有些情況該用 ReentrantLock:
- 需要 fair lock(公平性)
- 需要超時設定
- 需要 lock 多個條件變數
- 需要中斷執行緒
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); 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 的爛程式。
你的鼓勵將被轉換為我明天繼續加班的動力(真的)。 ❤️