synchronized 是 Java 並發編程基石,理解其鎖對象、作用域、重入性、與靜態方法交互細節,是寫出正確高效多線程代碼的前提。本文通過四個遞進實例,徹底講透 synchronized 核心機制。
this (實例方法) 或 Class 對象 (靜態方法)synchronized(obj) { ... } 手動指定鎖對象,粒度更細class Example {
public synchronized void execute() { // 加鎖
for (int i=0; i<10; i++) {
sleep(500);
System.out.println("Hello: " + i);
}
}
}
class Thread1 extends Thread {
Example ex;
public void run() { ex.execute(); }
}
public static void main(String[] args) {
Example ex = new Example();
new Thread1(ex).start();
new Thread1(ex).start(); // 共享同一 Example 實例
}
Hello: 0, Hello: 0, Hello: 1, Hello: 1... (並發)結論:實例同步方法鎖住 this,同一實例同一時間只能有一個線程進入任意同步方法。
class Example {
public synchronized void execute() { for(i..) print("Hello:"+i); }
public synchronized void execute2() { for(i..) print("World:"+i); }
}
new Thread1(ex).start(); // 呼叫 execute()
new Thread2(ex).start(); // 呼叫 execute2()
依然串行:先跑完 execute() 全部循環,再跑 execute2()。
鐵律:一個對象同一時間只能被一個線程持有其監視器鎖,該對象所有同步方法/塊互斥。
class Example {
public static synchronized void execute() { ... }
public static synchronized void execute2() { ... }
}
Example ex1 = new Example();
Example ex2 = new Example(); // 不同實例
new Thread1(ex1).start(); // 呼叫 Example.execute()
new Thread2(ex2).start(); // 呼叫 Example.execute2()
依然串行!因為靜態同步方法鎖住 Example.class,所有實例共享同一把 Class 鎖。
class Example {
private final Object lock = new Object(); // 專用鎖對象
public void execute() {
synchronized (lock) { // 僅鎖住關鍵區
for(i..) print("Hello:"+i);
}
// 這裡代碼可並發執行
}
public void execute2() {
synchronized (lock) { ... }
}
}
synchronized(lockA) vs synchronized(lockB)synchronized(this) { 方法體所有代碼 },但更靈活| 陷阱 | 後果 | 修正 |
|---|---|---|
| 鎖住 String 常量 / 包裝類 | 字串常量池/緩存導致非預期共享鎖 | 用 new Object() 專用鎖對象 |
| 同步塊中調用耗時操作 (IO、sleep) | 長時間持有鎖,嚴重降低並發度 | 縮小同步塊範圍,耗時操作移出 |
| 以為靜態/實例同步方法不互斥 | 實則鎖對象不同 (this vs Class),互不干擾 | 明確區分鎖對象,需跨類同步用共享鎖 |
| 死鎖 | 多線程循環等待對方鎖 | 固定獲取鎖順序、用 tryLock、避免巢狀鎖 |
thisClass 對象unlockReentrantLock、StampedLock、無鎖資料結構