目錄
什麼是 Lock?
Lock(鎖) 是併發程式設計中用來控制多個執行緒對共享資源存取的同步機制,確保在同一時間只有一個執行緒能夠存取受保護的資源。
為什麼需要 Lock?
沒有 Lock 的問題:
- Race Condition(競爭條件):多個執行緒同時修改資料導致不可預期結果
- 資料不一致:讀取到部分更新的資料
- Lost Update(更新遺失):一個執行緒的更新被另一個覆蓋
有了 Lock 的好處:
- 資料一致性:保證資料的正確性
- 執行緒安全:避免併發問題
- 可預測性:程式行為可控
核心特點
Lock = 互斥性 + 可見性 + 有序性
互斥性:同時只有一個執行緒持有鎖
可見性:釋放鎖時的修改對其他執行緒可見
有序性:確保操作的順序性
基本原理
原理 1:互斥(Mutual Exclusion)
說明:確保臨界區(Critical Section)同時只有一個執行緒執行
視覺化:
執行緒 A: [等待] → [獲取鎖] → [執行臨界區] → [釋放鎖]
執行緒 B: [等待] → [等待中...] → [等待中...] → [獲取鎖] → [執行臨界區]
範例:
// 臨界區:需要保護的程式碼區段
lock.lock();
try {
count++; // 臨界區:只有一個執行緒能執行
} finally {
lock.unlock();
}
原理 2:阻塞與喚醒
說明:無法獲取鎖的執行緒會被阻塞,等待鎖被釋放後喚醒
機制:
- 阻塞:執行緒進入等待佇列,放棄 CPU
- 喚醒:鎖釋放時,從等待佇列中選擇一個執行緒喚醒
- 公平性:決定喚醒順序的策略
原理 3:記憶體可見性
說明:鎖保證一個執行緒的修改對其他執行緒可見
Happens-Before 規則:
- 釋放鎖的操作 happens-before 後續獲取該鎖的操作
- 確保釋放鎖前的所有修改,在獲取鎖後都可見
核心 Lock 類型
1. Mutex Lock(互斥鎖)
定義:最基本的鎖,提供互斥存取
特點:
- 最簡單的鎖類型
- 只有兩種狀態:已鎖定(locked)、未鎖定(unlocked)
- 持有鎖的執行緒必須負責釋放
- 不可重入(需要特殊實作才支援)
使用場景:
- 保護簡單的臨界區
- 短時間的資源存取
- 不需要複雜功能的情況
範例:
// Go 中的 Mutex
var mu sync.Mutex
mu.Lock()
// 臨界區
mu.Unlock()
2. Reentrant Lock(可重入鎖/遞迴鎖)
定義:允許同一個執行緒多次獲取同一把鎖
特點:
- 追蹤持有鎖的執行緒和計數器
- 同一執行緒可以多次加鎖
- 必須解鎖相同次數才能真正釋放
- 避免自己鎖住自己的死鎖
重入機制:
執行緒 A 獲取鎖 → 計數器 = 1
執行緒 A 再次獲取 → 計數器 = 2
執行緒 A 釋放一次 → 計數器 = 1
執行緒 A 再釋放 → 計數器 = 0 → 鎖真正釋放
使用場景:
- 遞迴函數需要加鎖
- 方法內部互相呼叫且都需要鎖
- 需要在持有鎖時再次獲取鎖
範例:
ReentrantLock lock = new ReentrantLock();
public void outer() {
lock.lock();
try {
inner(); // 可以再次獲取鎖
} finally {
lock.unlock();
}
}
public void inner() {
lock.lock(); // 重入:同一執行緒可再次獲取
try {
// 執行操作
} finally {
lock.unlock();
}
}
3. Read-Write Lock(讀寫鎖)
定義:區分讀操作和寫操作,允許多個讀者或單一寫者
特點:
- 讀鎖(共享鎖):多個執行緒可同時持有
- 寫鎖(排他鎖):只有一個執行緒可持有,且阻塞所有讀鎖
- 提高讀多寫少場景的併發性能
- 寫操作時保證完全互斥
鎖的相容性:
讀鎖 寫鎖
讀鎖 ✅ ❌
寫鎖 ❌ ❌
使用場景:
- 讀操作遠多於寫操作
- 快取系統
- 資料庫查詢
- 配置檔案讀取
範例:
ReadWriteLock rwLock = new ReentrantReadWriteLock();
Lock readLock = rwLock.readLock();
Lock writeLock = rwLock.writeLock();
// 讀操作
public String read() {
readLock.lock();
try {
return data; // 多個執行緒可同時讀
} finally {
readLock.unlock();
}
}
// 寫操作
public void write(String value) {
writeLock.lock();
try {
data = value; // 獨佔存取
} finally {
writeLock.unlock();
}
}
Go 範例:
var rwMu sync.RWMutex
// 讀操作
func read() string {
rwMu.RLock()
defer rwMu.RUnlock()
return data
}
// 寫操作
func write(value string) {
rwMu.Lock()
defer rwMu.Unlock()
data = value
}
4. Spin Lock(自旋鎖)
定義:不阻塞執行緒,而是在迴圈中不斷檢查鎖是否可用
特點:
- 不進行執行緒上下文切換
- 持續佔用 CPU 資源
- 適合鎖持有時間極短的情況
- 減少上下文切換開銷
自旋 vs 阻塞:
自旋鎖:
while (鎖被持有) {
// 不斷檢查,持續佔用 CPU
}
阻塞鎖:
if (鎖被持有) {
執行緒進入睡眠 // 釋放 CPU,需要上下文切換
}
使用場景:
- 鎖持有時間非常短(微秒級)
- 臨界區代碼執行很快
- 多核心處理器環境
- 高效能需求
自適應自旋:
根據歷史經驗調整自旋時間:
- 如果過去成功獲取鎖 → 增加自旋時間
- 如果過去長時間等待 → 減少自旋時間,直接阻塞
範例:
// Java 中沒有直接的 SpinLock,但可以實作
public class SpinLock {
private AtomicBoolean locked = new AtomicBoolean(false);
public void lock() {
// 不斷嘗試直到成功
while (!locked.compareAndSet(false, true)) {
// 自旋等待
}
}
public void unlock() {
locked.set(false);
}
}
5. 樂觀鎖(Optimistic Lock)
定義:假設沒有衝突,在更新時才檢查是否有其他執行緒修改
特點:
- 不是真正的「鎖」,而是一種策略
- 使用版本號或 CAS(Compare-And-Swap)機制
- 沒有加鎖開銷
- 適合衝突少的場景
實作機制:
版本號機制
1. 讀取資料和版本號
2. 執行業務邏輯
3. 更新時檢查版本號是否改變
4. 如果沒變 → 更新並遞增版本號
如果改變 → 重試或失敗
CAS 機制
// Compare-And-Swap:比較並交換
int expected = 5;
int newValue = 6;
// 如果當前值是 expected,則更新為 newValue
atomicInt.compareAndSet(expected, newValue);
使用場景:
- 讀多寫少
- 衝突機率低
- 高併發場景
- 無鎖資料結構
範例:
// 使用 AtomicInteger 實現樂觀鎖
AtomicInteger count = new AtomicInteger(0);
// CAS 操作:無鎖的增加
public void increment() {
int current;
int next;
do {
current = count.get();
next = current + 1;
} while (!count.compareAndSet(current, next));
}
資料庫樂觀鎖:
-- 使用版本號
UPDATE products
SET stock = stock - 1, version = version + 1
WHERE id = 1 AND version = 10;
-- 如果 version 已經不是 10,更新失敗
6. 悲觀鎖(Pessimistic Lock)
定義:假設一定會發生衝突,每次存取資料時都加鎖
特點:
- 傳統的鎖機制(Mutex、Synchronized)
- 確保資料絕對安全
- 會降低併發性能
- 可能導致死鎖
與樂觀鎖對比:
悲觀鎖:
lock()
讀取資料
修改資料
寫入資料
unlock()
樂觀鎖:
讀取資料和版本號
修改資料
if (版本號未變) { 寫入 }
else { 重試 }
使用場景:
- 寫操作頻繁
- 衝突機率高
- 資料一致性要求極高
- 無法容忍重試成本
資料庫悲觀鎖:
-- 使用 SELECT FOR UPDATE
BEGIN TRANSACTION;
SELECT * FROM products WHERE id = 1 FOR UPDATE; -- 行鎖
-- 其他交易無法修改這一行
UPDATE products SET stock = stock - 1 WHERE id = 1;
COMMIT;
7. 公平鎖 vs 非公平鎖
定義:決定多個等待執行緒獲取鎖的順序
公平鎖(Fair Lock)
特點:
- 按照請求鎖的順序獲取鎖(FIFO)
- 不會有執行緒餓死
- 需要維護等待佇列
- 性能較差(需要排隊)
視覺化:
等待佇列: [執行緒 B] → [執行緒 C] → [執行緒 D]
鎖釋放 → 執行緒 B 獲取 → 嚴格按順序
非公平鎖(Non-Fair Lock)
特點:
- 新到的執行緒可以「插隊」嘗試獲取鎖
- 吞吐量更高
- 可能導致某些執行緒長時間等待(餓死)
- Java 預設使用非公平鎖
視覺化:
等待佇列: [執行緒 B] → [執行緒 C]
新執行緒 D 到達 → 直接嘗試獲取鎖 → 可能成功插隊
範例:
// 公平鎖:嚴格按照等待時間順序
ReentrantLock fairLock = new ReentrantLock(true);
// 非公平鎖:允許插隊
ReentrantLock unfairLock = new ReentrantLock(false);
選擇建議:
- 公平鎖:需要保證執行緒公平性、避免餓死
- 非公平鎖:追求性能、可以容忍偶爾的不公平
8. 偏向鎖、輕量級鎖、重量級鎖(JVM 優化)
說明:Java 虛擬機對 synchronized 的效能優化
偏向鎖(Biased Lock)
適用場景:只有一個執行緒存取
機制:記錄第一個獲取鎖的執行緒 ID
優化:該執行緒再次獲取時無需 CAS 操作
輕量級鎖(Lightweight Lock)
適用場景:少量執行緒競爭,且競爭不激烈
機制:使用 CAS 操作避免傳統互斥量
優化:減少上下文切換
重量級鎖(Heavyweight Lock)
適用場景:競爭激烈
機制:使用作業系統的互斥量(Mutex)
特點:執行緒阻塞,上下文切換
鎖升級過程:
無鎖 → 偏向鎖 → 輕量級鎖 → 重量級鎖
(只能升級,不能降級)
Java 中的 Lock 實作
1. synchronized 關鍵字
特點:
- Java 內建的同步機制
- 自動獲取和釋放鎖
- 可重入
- 非公平鎖(JVM 優化後)
三種用法:
// 1. 同步方法
public synchronized void method() {
// 鎖定 this 物件
}
// 2. 同步靜態方法
public static synchronized void staticMethod() {
// 鎖定 Class 物件
}
// 3. 同步程式碼區塊
public void method() {
synchronized(this) {
// 明確指定鎖物件
}
}
優缺點:
✅ 優點:
- 簡單易用
- 自動釋放鎖(不會忘記 unlock)
- JVM 優化良好
❌ 缺點:
- 無法中斷等待鎖的執行緒
- 無法設定逾時
- 無法實作公平鎖
- 功能較少
2. ReentrantLock
特點:
- 更靈活的鎖實作
- 可中斷、可逾時
- 可選公平/非公平
- 需手動釋放鎖
基本用法:
ReentrantLock lock = new ReentrantLock();
lock.lock(); // 獲取鎖
try {
// 臨界區
} finally {
lock.unlock(); // 必須在 finally 中釋放
}
進階功能:
// 1. 可中斷的鎖獲取
try {
lock.lockInterruptibly();
try {
// 臨界區
} finally {
lock.unlock();
}
} catch (InterruptedException e) {
// 等待鎖時被中斷
}
// 2. 嘗試獲取鎖(非阻塞)
if (lock.tryLock()) {
try {
// 獲取到鎖
} finally {
lock.unlock();
}
} else {
// 未獲取到鎖,執行其他邏輯
}
// 3. 逾時獲取鎖
if (lock.tryLock(1, TimeUnit.SECONDS)) {
try {
// 在 1 秒內獲取到鎖
} finally {
lock.unlock();
}
} else {
// 逾時,未獲取到鎖
}
3. ReentrantReadWriteLock
讀寫分離的鎖實作:
ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
Lock readLock = rwLock.readLock();
Lock writeLock = rwLock.writeLock();
// 讀操作
public String getData() {
readLock.lock();
try {
return data;
} finally {
readLock.unlock();
}
}
// 寫操作
public void setData(String value) {
writeLock.lock();
try {
data = value;
} finally {
writeLock.unlock();
}
}
4. StampedLock(Java 8+)
特點:
- 比 ReentrantReadWriteLock 更快
- 支援樂觀讀
- 不可重入
三種鎖模式:
StampedLock lock = new StampedLock();
// 1. 寫鎖(排他鎖)
long stamp = lock.writeLock();
try {
// 修改資料
} finally {
lock.unlockWrite(stamp);
}
// 2. 讀鎖(悲觀讀)
long stamp = lock.readLock();
try {
// 讀取資料
} finally {
lock.unlockRead(stamp);
}
// 3. 樂觀讀(最快)
long stamp = lock.tryOptimisticRead();
// 讀取資料
if (!lock.validate(stamp)) {
// 資料可能被修改,升級為悲觀讀
stamp = lock.readLock();
try {
// 重新讀取
} finally {
lock.unlockRead(stamp);
}
}
Go 中的 Lock 實作
1. sync.Mutex
基本互斥鎖:
var mu sync.Mutex
func update() {
mu.Lock()
defer mu.Unlock()
// 臨界區
}
特點:
- 不可重入(重入會造成死鎖)
- 零值可用
- 不能複製已使用的 Mutex
2. sync.RWMutex
讀寫鎖:
var rwMu sync.RWMutex
// 讀操作
func read() string {
rwMu.RLock()
defer rwMu.RUnlock()
return data
}
// 寫操作
func write(value string) {
rwMu.Lock()
defer rwMu.Unlock()
data = value
}
3. Channel(Go 的推薦方式)
使用 Channel 實現同步:
// 使用 buffered channel 作為互斥鎖
type Mutex chan struct{}
func NewMutex() Mutex {
ch := make(chan struct{}, 1)
ch <- struct{}{} // 初始化為「未鎖定」
return ch
}
func (m Mutex) Lock() {
<-m // 接收,獲取鎖
}
func (m Mutex) Unlock() {
m <- struct{}{} // 發送,釋放鎖
}
Go 哲學:
"Don't communicate by sharing memory;
share memory by communicating."
不要透過共享記憶體來通訊;
而是透過通訊來共享記憶體。
Lock 類型比較
特性對比表
| Lock 類型 | 可重入 | 公平性 | 讀寫分離 | 樂觀/悲觀 | 阻塞方式 | 適用場景 |
|---|---|---|---|---|---|---|
| Mutex | ❌ | 非公平 | ❌ | 悲觀 | 阻塞 | 簡單互斥 |
| ReentrantLock | ✅ | 可選 | ❌ | 悲觀 | 可選 | 通用場景 |
| ReadWriteLock | ✅ | 可選 | ✅ | 悲觀 | 阻塞 | 讀多寫少 |
| SpinLock | ❌ | 非公平 | ❌ | 悲觀 | 自旋 | 極短臨界區 |
| 樂觀鎖 | N/A | N/A | N/A | 樂觀 | 不阻塞 | 衝突少 |
| synchronized | ✅ | 非公平 | ❌ | 悲觀 | 阻塞 | Java 簡單場景 |
| StampedLock | ❌ | 非公平 | ✅ | 可選 | 阻塞 | 高效能讀多 |
效能對比
低競爭場景(1-2 個執行緒):
樂觀鎖 > SpinLock > synchronized > ReentrantLock
中度競爭場景(3-10 個執行緒):
ReadWriteLock > ReentrantLock > synchronized > SpinLock
高競爭場景(10+ 個執行緒):
StampedLock > ReadWriteLock > ReentrantLock > SpinLock ❌
注意:SpinLock 在高競爭場景會浪費大量 CPU
選擇決策樹
需要同步?
├─ 讀多寫少?
│ ├─ 是 → ReadWriteLock 或 StampedLock
│ └─ 否 → 繼續判斷
├─ 需要高級功能(逾時、中斷、公平性)?
│ ├─ 是 → ReentrantLock
│ └─ 否 → synchronized 或 Mutex
├─ 衝突非常少?
│ ├─ 是 → 樂觀鎖(CAS)
│ └─ 否 → 悲觀鎖
└─ 臨界區極短(微秒級)且多核心?
├─ 是 → SpinLock
└─ 否 → 阻塞鎖
實戰範例
範例 1:銀行帳戶轉帳(避免死鎖)
問題:兩個帳戶互相轉帳可能造成死鎖
// ❌ 錯誤示範:可能死鎖
public void transfer(Account from, Account to, int amount) {
synchronized(from) {
synchronized(to) {
from.debit(amount);
to.credit(amount);
}
}
}
// 執行緒 A: transfer(account1, account2, 100)
// 執行緒 B: transfer(account2, account1, 200)
// → 死鎖!
解決方案:使用固定順序獲取鎖
// ✅ 正確做法:按照固定順序加鎖
public void transfer(Account from, Account to, int amount) {
Account first = from.id < to.id ? from : to;
Account second = from.id < to.id ? to : from;
synchronized(first) {
synchronized(second) {
from.debit(amount);
to.credit(amount);
}
}
}
範例 2:快取系統(讀寫鎖)
public class Cache<K, V> {
private final Map<K, V> map = new HashMap<>();
private final ReadWriteLock lock = new ReentrantReadWriteLock();
public V get(K key) {
lock.readLock().lock();
try {
return map.get(key);
} finally {
lock.readLock().unlock();
}
}
public void put(K key, V value) {
lock.writeLock().lock();
try {
map.put(key, value);
} finally {
lock.writeLock().unlock();
}
}
}
範例 3:雙重檢查鎖定(Double-Checked Locking)
延遲初始化的線程安全單例模式:
public class Singleton {
// 必須使用 volatile 確保可見性
private static volatile Singleton instance;
public static Singleton getInstance() {
// 第一次檢查:避免不必要的同步
if (instance == null) {
synchronized(Singleton.class) {
// 第二次檢查:確保只創建一次
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
為什麼需要 volatile?
沒有 volatile 的問題:
1. 執行緒 A 創建物件(分配記憶體 → 初始化 → 賦值給 instance)
2. 由於指令重排序,可能先賦值,後初始化
3. 執行緒 B 看到 instance 不為 null,但實際未初始化完成
4. 執行緒 B 使用未初始化的物件 → 錯誤!
volatile 的作用:
- 禁止指令重排序
- 確保可見性
範例 4:生產者-消費者(Condition)
public class BoundedBuffer<T> {
private final Queue<T> queue = new LinkedList<>();
private final int capacity;
private final Lock lock = new ReentrantLock();
private final Condition notFull = lock.newCondition();
private final Condition notEmpty = lock.newCondition();
public BoundedBuffer(int capacity) {
this.capacity = capacity;
}
// 生產者
public void put(T item) throws InterruptedException {
lock.lock();
try {
while (queue.size() == capacity) {
notFull.await(); // 等待不滿
}
queue.offer(item);
notEmpty.signal(); // 通知消費者
} finally {
lock.unlock();
}
}
// 消費者
public T take() throws InterruptedException {
lock.lock();
try {
while (queue.isEmpty()) {
notEmpty.await(); // 等待不空
}
T item = queue.poll();
notFull.signal(); // 通知生產者
return item;
} finally {
lock.unlock();
}
}
}
範例 5:樂觀鎖實作庫存扣減
public class Inventory {
private AtomicInteger stock = new AtomicInteger(100);
// 使用 CAS 實現無鎖扣減
public boolean decreaseStock(int quantity) {
while (true) {
int current = stock.get();
if (current < quantity) {
return false; // 庫存不足
}
int next = current - quantity;
if (stock.compareAndSet(current, next)) {
return true; // 扣減成功
}
// CAS 失敗,重試
}
}
}
最佳實踐
1. 最小化鎖的範圍
// ❌ 不好:鎖的範圍太大
public synchronized void process() {
// 1. 長時間的計算(不需要鎖)
int result = complexCalculation();
// 2. 修改共享資料(需要鎖)
sharedData.update(result);
// 3. I/O 操作(不需要鎖)
writeToFile(result);
}
// ✅ 好:只鎖必要的部分
public void process() {
// 1. 不需要鎖
int result = complexCalculation();
// 2. 只鎖這部分
synchronized(this) {
sharedData.update(result);
}
// 3. 不需要鎖
writeToFile(result);
}
2. 避免在鎖內調用未知方法
// ❌ 危險:可能導致死鎖或性能問題
public synchronized void dangerous() {
// 調用外部方法,不知道內部會做什麼
externalLibrary.someMethod(); // 可能內部也加鎖!
}
// ✅ 安全:先獲取需要的資料,再調用外部方法
public void safe() {
Data data;
synchronized(this) {
data = getData();
}
// 在鎖外調用
externalLibrary.process(data);
}
3. 使用 try-finally 確保鎖釋放
// ✅ 正確模式
Lock lock = new ReentrantLock();
lock.lock();
try {
// 臨界區
} finally {
lock.unlock(); // 確保一定會釋放
}
4. 避免鎖嵌套
// ❌ 容易死鎖
synchronized(lockA) {
synchronized(lockB) {
// ...
}
}
// ✅ 如果必須嵌套,確保順序一致
// 所有地方都按 A → B 的順序獲取鎖
5. 使用逾時避免永久阻塞
// ✅ 設定逾時
if (lock.tryLock(1, TimeUnit.SECONDS)) {
try {
// 執行操作
} finally {
lock.unlock();
}
} else {
// 處理無法獲取鎖的情況
log.warn("無法獲取鎖,放棄操作");
}
6. 適當使用鎖粒度
// ❌ 粒度太粗:整個物件一個鎖
public class UserService {
private synchronized void updateUser(User user) { }
private synchronized void deleteUser(User user) { }
}
// ✅ 粒度適當:不同資料使用不同鎖
public class UserService {
private final Map<Long, Lock> userLocks = new ConcurrentHashMap<>();
public void updateUser(User user) {
Lock lock = userLocks.computeIfAbsent(user.getId(),
k -> new ReentrantLock());
lock.lock();
try {
// 只鎖定特定使用者
} finally {
lock.unlock();
}
}
}
7. 選擇合適的鎖類型
決策指南:
簡單場景 → synchronized
├─ 優點:簡單、自動釋放
└─ 缺點:功能少
需要高級功能 → ReentrantLock
├─ 逾時
├─ 可中斷
└─ 公平性
讀多寫少 → ReadWriteLock
└─ 提高讀併發性能
極致效能 + 讀多 → StampedLock
└─ 支援樂觀讀
衝突少 → 樂觀鎖(AtomicXxx)
└─ 無鎖,最高效能
常見問題
問題 1:什麼時候應該使用 synchronized,什麼時候用 ReentrantLock?
回答:
使用 synchronized:
- 簡單的同步需求
- 不需要逾時、中斷功能
- 希望程式碼簡潔
- JVM 會自動優化
使用 ReentrantLock:
- 需要嘗試獲取鎖(tryLock)
- 需要可中斷的鎖獲取
- 需要設定逾時
- 需要公平鎖
- 需要使用 Condition
經驗法則:預設使用 synchronized,需要高級功能時才用 ReentrantLock
問題 2:ReadWriteLock 真的能提升效能嗎?
回答:取決於讀寫比例
適合使用的場景:
讀操作 : 寫操作 > 10:1
效能對比:
場景:100 次操作,90 讀 10 寫
Mutex:100 次都串行 → 慢
ReadWriteLock:90 次可並行,10 次串行 → 快
場景:100 次操作,50 讀 50 寫
Mutex:100 次串行
ReadWriteLock:50 次可並行,50 次串行 + 管理開銷 → 差異不大
注意:
- 寫操作多時,ReadWriteLock 的管理開銷可能更大
- 需要實際測試驗證
問題 3:什麼是死鎖?如何避免?
死鎖定義:兩個或多個執行緒互相等待對方持有的鎖
經典範例:
執行緒 A:持有鎖 X,等待鎖 Y
執行緒 B:持有鎖 Y,等待鎖 X
→ 永久等待,死鎖!
死鎖的四個必要條件:
- 互斥:資源不能共享
- 持有並等待:持有一個資源,等待另一個
- 不可剝奪:不能強制釋放資源
- 循環等待:存在循環等待鏈
避免死鎖的方法:
方法 1:固定加鎖順序
// 所有地方都按相同順序獲取鎖
synchronized(lockA) {
synchronized(lockB) {
// ...
}
}
方法 2:使用逾時
if (lockA.tryLock(100, TimeUnit.MILLISECONDS)) {
try {
if (lockB.tryLock(100, TimeUnit.MILLISECONDS)) {
try {
// 執行操作
} finally {
lockB.unlock();
}
}
} finally {
lockA.unlock();
}
}
方法 3:使用更大粒度的鎖
// 用一個更大的鎖替代多個小鎖
synchronized(globalLock) {
// 同時操作多個資源
}
方法 4:使用無鎖資料結構
// 使用 ConcurrentHashMap 替代 synchronized + HashMap
問題 4:volatile 和 synchronized 有什麼區別?
對比:
| 特性 | volatile | synchronized |
|---|---|---|
| 作用 | 確保可見性、禁止重排序 | 確保原子性、可見性、有序性 |
| 使用 | 只能修飾變數 | 可修飾方法、程式碼區塊 |
| 原子性 | ❌ 不保證 | ✅ 保證 |
| 阻塞 | ❌ 不會阻塞 | ✅ 可能阻塞 |
| 性能 | 更快 | 較慢 |
| 應用 | 狀態標記、雙重檢查 | 臨界區保護 |
volatile 適用場景:
// ✅ 適合:簡單的狀態標記
private volatile boolean running = true;
public void stop() {
running = false; // 寫操作
}
public void run() {
while (running) { // 讀操作
// ...
}
}
// ❌ 不適合:複合操作
private volatile int count = 0;
count++; // 不是原子操作!讀取-修改-寫入
問題 5:為什麼 Go 不推薦使用共享記憶體?
回答:Go 推薦使用 Channel 而非 Lock
原因:
- 更簡單:避免鎖的複雜性
- 避免死鎖:不需要考慮鎖順序
- 符合 CSP 模型:Communicating Sequential Processes
- 更安全:編譯器檢查
對比:
// ❌ 使用 Mutex(容易出錯)
var mu sync.Mutex
var data int
func update() {
mu.Lock()
data++ // 忘記 Unlock 怎麼辦?
mu.Unlock()
}
// ✅ 使用 Channel(更安全)
func worker(dataChan chan int) {
for data := range dataChan {
// 處理資料
}
}
何時使用 Mutex?
- 簡單的計數器
- 快取
- 效能關鍵路徑(Channel 有開銷)
總結
核心要點
Lock 的本質 = 控制對共享資源的存取
三大保證:
├─ 互斥性:同時只有一個執行緒
├─ 可見性:修改對其他執行緒可見
└─ 有序性:確保操作順序
鎖的選擇原則
1. 預設選擇
└─ Java: synchronized / Go: Channel
2. 需要高級功能
└─ ReentrantLock (Java) / Mutex (Go)
3. 讀多寫少
└─ ReadWriteLock / RWMutex
4. 追求極致效能
└─ StampedLock / 樂觀鎖
5. 臨界區極短
└─ SpinLock
6. 衝突很少
└─ 樂觀鎖(CAS)
最佳實踐速查
| 原則 | 說明 |
|---|---|
| 最小化鎖範圍 | 只鎖必要的程式碼 |
| 避免鎖嵌套 | 減少死鎖風險 |
| 固定加鎖順序 | 避免循環等待 |
| 使用 try-finally | 確保鎖釋放 |
| 設定逾時 | 避免永久阻塞 |
| 選對鎖類型 | 根據場景選擇 |
| 考慮無鎖方案 | AtomicXxx, Channel |
常見錯誤
❌ 錯誤做法:
- 忘記釋放鎖
- 在鎖內執行耗時操作
- 不一致的鎖順序
- 過度使用鎖(鎖粒度太粗)
- 在不需要時使用重量級鎖
✅ 正確做法:
- 使用 try-finally 確保釋放
- 最小化鎖範圍
- 統一加鎖順序
- 適當的鎖粒度
- 根據場景選擇合適的鎖類型
建立日期:2025-11-19 最後更新:2025-11-19