Lock 鎖機制完整指南

深入理解程式中常用的鎖機制,包含原理、類型、使用場景與比較分析


目錄


什麼是 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. 互斥:資源不能共享
  2. 持有並等待:持有一個資源,等待另一個
  3. 不可剝奪:不能強制釋放資源
  4. 循環等待:存在循環等待鏈

避免死鎖的方法

方法 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

原因

  1. 更簡單:避免鎖的複雜性
  2. 避免死鎖:不需要考慮鎖順序
  3. 符合 CSP 模型:Communicating Sequential Processes
  4. 更安全:編譯器檢查

對比

// ❌ 使用 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

🔗相關文章