CodeGym /課程 /JAVA 25 SELF /ReentrantLock 與 ReadWriteLock: 差異、範例

ReentrantLock 與 ReadWriteLock: 差異、範例

JAVA 25 SELF
等級 52 , 課堂 2
開放

1. ReentrantLock 類:彈性的鎖定

關鍵字 synchronized 很適合基本情境:可以快速、簡單地保護方法或程式碼區塊。但有時我們需要更多:

  • 顯式管理鎖(例如嘗試取得鎖,若失敗就不等待)。
  • 將資源的「讀取」與「寫入」權限分離。
  • 可中斷等待鎖。
  • 診斷是誰、何時取得或釋放了鎖。

針對這些需求,ReentrantLockReadWriteLock 應運而生。它們比傳統的 synchronized 提供更多控制與功能。

它到底是什麼?

ReentrantLock 是實作介面 Lock 的類別。它的運作大致和 synchronized 類似,但有更多「配備」。最大的差異是鎖的控制變為顯式:你需要自行呼叫 lock()unlock()

有個重點——reentrant 的意思是「可重入」,同一個執行緒可以連續多次取得同一把鎖而不導致死鎖。當方法遞迴呼叫自身,或在呼叫鏈中共用同一把鎖時,這會很有用。

使用語法

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class Counter {
    private int value = 0;
    private final Lock lock = new ReentrantLock();

    public void increment() {
        lock.lock(); // 取得鎖
        try {
            value++;
        } finally {
            lock.unlock(); // 務必釋放鎖!
        }
    }

    public int getValue() {
        lock.lock();
        try {
            return value;
        } finally {
            lock.unlock();
        }
    }
}

請注意:
呼叫 lock()unlock() 時,一定要搭配 try...finally。若忘了呼叫 unlock(),其他執行緒將無法進入受保護的區塊——會造成永無止盡的阻塞。

ReentrantLock 的功能

嘗試取得:
可以嘗試取得鎖,但不要無限期等待:

if (lock.tryLock()) {
    try {
        // 執行工作
    } finally {
        lock.unlock();
    }
} else {
    // 未取得鎖—改做其他事
}

帶逾時的等待:

if (lock.tryLock(100, TimeUnit.MILLISECONDS)) {
    // 在 100 毫秒內取得鎖
}

檢查鎖是否已被持有:

if (lock.isLocked()) { ... }

等待佇列診斷、「公平性」鎖模式 等其他實用功能。

3. 範例:使用 ReentrantLock 遞增計數器

讓我們擴充主控台應用程式(例如模擬來自不同執行緒的訂單處理),比較使用 synchronizedReentrantLock 的寫法。

使用 synchronized 的範例

public class OrderCounter {
    private int count = 0;

    public synchronized void increment() {
        count++;
    }

    public synchronized int getCount() {
        return count;
    }
}

使用 ReentrantLock 的等價範例

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class OrderCounter {
    private int count = 0;
    private final Lock lock = new ReentrantLock();

    public void increment() {
        lock.lock();
        try {
            count++;
        } finally {
            lock.unlock();
        }
    }

    public int getCount() {
        lock.lock();
        try {
            return count;
        } finally {
            lock.unlock();
        }
    }
}

有什麼好處?

  • 可嘗試取得鎖而不必無限期等待(tryLock())。
  • 能實作更複雜的邏輯:例如以特定順序取得多把鎖(適用於複雜資料結構)。
  • 可以在不同位置「解鎖」(但務必非常謹慎——一定要記得 unlock()!)。

4. ReadWriteLock:讀寫鎖

它是什麼?

ReadWriteLock 不只是鎖,更像是聰明的存取協調器。其主要實作是 ReentrantReadWriteLock,並將鎖分為兩類:讀鎖與寫鎖。

當執行緒只進行讀取且沒有人修改資料時,可以同時並行——讀取不會互相干擾。但一旦有人要寫入,其餘都必須等待:寫入只允許單一參與者,且需要獨占。

這在「讀多寫少」的情境特別有用——例如使用者經常瀏覽的商品目錄,但只偶爾更新。

使用語法

import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

public class ProductCatalog {
    private final Map<String, String> products = new HashMap<>();
    private final ReadWriteLock rwLock = new ReentrantReadWriteLock();

    public void addProduct(String id, String name) {
        rwLock.writeLock().lock();
        try {
            products.put(id, name);
        } finally {
            rwLock.writeLock().unlock();
        }
    }

    public String getProduct(String id) {
        rwLock.readLock().lock();
        try {
            return products.get(id);
        } finally {
            rwLock.readLock().unlock();
        }
    }
}

在我們的應用中的使用範例

假設我們有一個訂單資料庫,所有執行緒會讀取(例如搜尋訂單),但偶爾會有新訂單加入(寫入操作)。

import java.util.*;
import java.util.concurrent.locks.*;

public class OrderDatabase {
    private final List<String> orders = new ArrayList<>();
    private final ReadWriteLock rwLock = new ReentrantReadWriteLock();

    // 新增訂單(需要 writeLock)
    public void addOrder(String order) {
        rwLock.writeLock().lock();
        try {
            orders.add(order);
        } finally {
            rwLock.writeLock().unlock();
        }
    }

    // 取得所有訂單的副本(可並行讀取)
    public List<String> getOrders() {
        rwLock.readLock().lock();
        try {
            // 回傳副本以避免競態
            return new ArrayList<>(orders);
        } finally {
            rwLock.readLock().unlock();
        }
    }
}

發生了什麼?

  • 只要沒有寫入,成千上萬的執行緒都能同時讀取訂單清單。
  • 一旦某個執行緒開始新增訂單——讀取就會被封鎖,以避免出現不一致的資料。

5. 比較:何時該用哪一種?

情境 synchronized ReentrantLock ReadWriteLock
簡單同步 ✖(過度)
需要逾時/嘗試取得
讀多寫少 ✔(顯著提升)
需要診斷/統計
遞迴鎖定 ✔(可重入性)

結論:

  • 簡單情境——使用 synchronized
  • 需要彈性——ReentrantLock
  • 「常讀少寫」——ReadWriteLock

6. 視覺化:ReadWriteLock 的運作示意

flowchart LR
    subgraph 讀取
      T1[執行緒 1] -- 讀取 --> Orders
      T2[執行緒 2] -- 讀取 --> Orders
      T3[執行緒 3] -- 讀取 --> Orders
    end
    subgraph 寫入
      T4[執行緒 4] -- 寫入 (addOrder) --> Orders
    end
    Orders[訂單清單]
    style Orders fill:#f9f,stroke:#333,stroke-width:2px

只要沒有任何執行緒在寫入,大家就能同時讀取。一旦出現寫入,其餘執行緒要等到 writeLock 完成。

7. 實作細節與注意事項

「公平性」(fairness)

ReentrantLockReentrantReadWriteLock 中,可以啟用「公平」模式(fair mode):執行緒依佇列順序服務,而非搶占式的「誰搶到就先用」。這能避免執行緒飢餓,但可能降低效能。

Lock fairLock = new ReentrantLock(true); // true — 公平模式
ReadWriteLock fairRWLock = new ReentrantReadWriteLock(true);

潛在陷阱

  • 遺漏 unlock: 若未呼叫 unlock(),將造成永久鎖定。務必使用 try...finally
  • 鎖內發生例外: 即使程式區塊內拋出例外,鎖仍必須被釋放!
  • 過度使用 ReadWriteLock: 對於小型集合,或幾乎總是寫入的情況——ReadWriteLock 意義不大,反而讓程式碼更複雜。

8. 常見錯誤

錯誤 №1:忘了呼叫 unlock()
最常見、也最棘手的錯誤,是在取得鎖後忘記呼叫 unlock()。結果就是永久鎖定,執行緒「卡住」。即使覺得「這裡不會出事」,也要一律使用 try...finally

錯誤 №2:在不需要的地方使用 ReadWriteLock
如果幾乎沒有平行讀取,而寫入很頻繁,ReadWriteLock 只會讓程式碼更複雜且降低效能。僅在確實存在大量同時讀取時才使用它。

錯誤 №3:以不同順序取得多把鎖
若你的程式碼會取得多把 Lock(例如對多個物件),請在所有執行緒中一律以相同的順序取得。否則可能發生 deadlock——執行緒會彼此無限等待。

錯誤 №4:把 synchronized 隨便換成 ReentrantLock
不要不加思索地把所有 synchronized 都換成 Lock——這不一定會加速程式,還可能讓程式碼更難閱讀。

錯誤 №5:忽略可重入性
同一個執行緒連續多次呼叫 lock()ReentrantLock 來說是正常的,但別忘了 unlock() 需要呼叫相同次數!

留言
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION