博客 / 詳情

返回

深入剖析 Java ReentrantLock:解鎖顯式鎖的高級特性與實戰應用

一、鎖的進化:從 synchronized 到 ReentrantLock

大家好,在多線程編程中,鎖機制是保證線程安全的核心技術。Java 早期只提供了 synchronized 這一種內置鎖,而在 JDK 1.5 後,Doug Lea 大師為我們帶來了更加靈活強大的顯式鎖ReentrantLock

synchronized 雖然用起來簡單,但在某些場景下會顯得"能力不足":

  • 無法響應中斷請求
  • 無法嘗試獲取鎖
  • 不支持公平性選擇
  • 通知機制基於單一等待隊列,難以實現精準喚醒

這時,ReentrantLock就成了我們的"救星"。讓我們一起來深入瞭解這把鎖!

二、ReentrantLock 的核心特性

ReentrantLock 是 Lock 接口的一個實現,它提供了比 synchronized 更豐富的功能:

graph TD
    A[Lock接口] --> B[ReentrantLock]
    B --> C[公平鎖]
    B --> D[非公平鎖]
    A --> E[ReadWriteLock接口] --> F[ReentrantReadWriteLock]
    A -.-> G[其他實現...]

2.1 可重入性

首先,什麼是"可重入"?簡單説就是:同一個線程可以多次獲取同一把鎖而不會死鎖

舉個生活例子:小明進入自己房間後反鎖了門,這時他想去衞生間,衞生間的門也是需要鑰匙的,而這把鑰匙就在小明口袋裏。如果鎖是"不可重入"的,那麼小明就陷入了困境——他無法使用口袋裏的鑰匙,因為他已經在使用這把鑰匙鎖住了房門。

但在"可重入鎖"的情況下,小明可以直接用同一把鑰匙開衞生間的門,而不會有任何問題。

來看代碼示例:

public class ReentrantDemo {
    private final ReentrantLock lock = new ReentrantLock();

    public void outer() {
        lock.lock();  // 第一次獲取鎖
        try {
            System.out.println("進入outer方法,當前線程:" + Thread.currentThread().getName());
            inner();  // 調用inner方法
        } finally {
            lock.unlock();  // 釋放鎖
        }
    }

    public void inner() {
        lock.lock();  // 第二次獲取鎖(同一線程)
        try {
            System.out.println("進入inner方法,當前線程:" + Thread.currentThread().getName());
        } finally {
            lock.unlock();  // 釋放鎖
        }
    }

    public static void main(String[] args) {
        ReentrantDemo demo = new ReentrantDemo();
        demo.outer();
    }
}

如果沒有可重入特性,上面代碼在調用 inner()方法時就會死鎖!

為了更直觀地理解可重入性的重要性,看一個模擬"不可重入鎖"的例子:

public class NonReentrantLockDemo {
    // 模擬一個不可重入鎖
    private static class NonReentrantLock {
        private boolean isLocked = false;
        private Thread lockedBy = null;

        public synchronized void lock() throws InterruptedException {
            // 不管是否是當前持有鎖的線程,都要等待鎖釋放
            while (isLocked) {
                wait();
            }
            isLocked = true;
            lockedBy = Thread.currentThread();
        }

        public synchronized void unlock() {
            if (isLocked && Thread.currentThread() == lockedBy) {
                isLocked = false;
                lockedBy = null;
                notify();
            }
        }
    }

    private static final NonReentrantLock nonReentrantLock = new NonReentrantLock();

    public static void main(String[] args) throws InterruptedException {
        nonReentrantLock.lock();
        System.out.println("獲取第一次鎖");

        try {
            // 嘗試再次獲取鎖
            System.out.println("嘗試獲取第二次鎖...");
            nonReentrantLock.lock();  // 這裏會永久阻塞!
            System.out.println("獲取第二次鎖成功"); // 永遠不會執行到這裏
        } finally {
            nonReentrantLock.unlock();
        }
    }
}

運行這段代碼會永久阻塞,因為第二次調用lock()時,鎖已被同一線程持有,但由於不支持重入,線程只能等待自己釋放鎖,形成死鎖。這正是可重入性解決的問題。

2.2 公平鎖與非公平鎖

ReentrantLock 提供了兩種獲取鎖的方式:公平鎖和非公平鎖。

graph TD
    A[ReentrantLock] --> B[非公平鎖默認]
    A --> C[公平鎖]
    B -- "lock()" --> D[立即嘗試搶佔鎖]
    D -- "失敗" --> E[進入隊列等待]
    C -- "lock()" --> F[嚴格按照等待隊列FIFO獲取鎖]
  • 公平鎖:嚴格按照線程請求的順序獲取鎖,類似於排隊買票,先來先得
  • 非公平鎖:不保證等待時間最長的線程優先獲取鎖,允許"插隊",默認模式

創建方式對比:

// 默認創建非公平鎖
ReentrantLock unfairLock = new ReentrantLock();

// 創建公平鎖
ReentrantLock fairLock = new ReentrantLock(true);

公平鎖的優點是顯著降低了"飢餓"現象發生的概率,保證每個線程都有機會獲取鎖;缺點是整體吞吐量相對較低。非公平鎖則允許更充分地利用 CPU 資源,但可能導致某些線程長時間等待。

需要注意的是,即使使用公平鎖,也無法完全杜絕飢餓現象,因為線程可能因為其他原因(如中斷或取消)退出等待隊列。

場景選擇建議

  • 在高併發且線程生命週期較短的場景中,非公平鎖通常表現更好,因為新線程可以立即嘗試獲取鎖,減少上下文切換
  • 在線程任務執行時間差異大、並且某些線程優先級較低的系統中,公平鎖可以減少低優先級線程的飢餓概率
  • 對於需要嚴格保證請求順序的系統(如排隊系統),公平鎖是更合適的選擇

2.3 多種獲取鎖的方式

ReentrantLock 提供了多種獲取鎖的方式,大大增強了靈活性:

  1. lock():最基本的獲取鎖方法,如果鎖被佔用,會一直等待
  2. tryLock():嘗試獲取鎖,立即返回結果(成功/失敗),不會阻塞
  3. tryLock(long timeout, TimeUnit unit):在指定時間內嘗試獲取鎖
  4. lockInterruptibly():可中斷的獲取鎖,允許在等待時響應中斷信號

我們可以用一個餐廳排隊的例子來理解:

  • lock():不管多久我都要等到有位置
  • tryLock():看一眼有沒有空位,有就坐,沒有就走
  • tryLock(time):最多等 30 分鐘,如果還沒位置就去別家
  • lockInterruptibly():等位過程中如果接到重要電話可以中途離開

2.4 精準通知機制:Condition

ReentrantLock 結合 Condition 接口,提供了比 synchronized + wait/notify 更加強大的線程通信能力:

graph LR
    A[ReentrantLock] -- "創建" --> B[Condition A]
    A -- "創建" --> C[Condition B]
    A -- "創建" --> D[Condition C]
    B -- "await/signal" --> E[線程1]
    C -- "await/signal" --> F[線程2]
    D -- "await/signal" --> G[線程3]

與 synchronized 相比的優勢:

  • 一個鎖可以創建多個 Condition 對象,實現"選擇性通知"
  • 更精準的線程控制,避免了 Object.notify()的盲目喚醒
  • 提供帶超時的等待和可中斷的等待

信號類型對比

  • signal():只喚醒單個等待該條件的線程,適用於只需要喚醒一個消費者/生產者的場景
  • signalAll():喚醒所有等待該條件的線程,適用於需要通知所有相關線程的狀態變更場景

重要提示Conditionawait()signal()方法必須在持有鎖的情況下調用,否則會拋出IllegalMonitorStateException。這一點與synchronized中的wait()/notify()要求一致。

Condition 還提供了帶超時的等待方法:

  • await(long time, TimeUnit unit):在指定時間內等待,超時或被通知則返回

這進一步增強了線程等待的靈活性,避免了無限期阻塞的風險。

2.5 鎖狀態查詢能力

ReentrantLock 提供了一系列查詢鎖狀態的方法,這在調試和監控中非常有用:

  • isLocked():查詢鎖是否被任何線程持有
  • isHeldByCurrentThread():查詢當前線程是否持有鎖
  • getHoldCount():查詢當前線程持有鎖的次數(重入次數)
  • getQueueLength():獲取等待獲取此鎖的線程數
  • hasQueuedThread(Thread t):查詢指定線程是否在等待隊列中

這些方法讓我們能夠更精確地瞭解鎖的使用狀態,在複雜併發場景中進行故障排查。

三、ReentrantLock 實戰案例

3.1 案例 1:實現可中斷的獲取鎖

當多個線程競爭鎖時,如果使用lockInterruptibly()方法,我們可以實現提前結束等待狀態,避免死鎖:

public class InterruptibleLockDemo {
    private static final ReentrantLock lock = new ReentrantLock();

    public static void main(String[] args) throws InterruptedException {
        Thread thread1 = new Thread(() -> {
            try {
                lock.lock();
                System.out.println("線程1獲取到鎖,將無限期持有...");
                // 模擬長時間持有鎖
                Thread.sleep(Integer.MAX_VALUE);
            } catch (InterruptedException e) {
                System.out.println("線程1被中斷");
                // 此處不恢復中斷狀態,因為線程需要繼續持有鎖而不被中斷
            } finally {
                lock.unlock();
                System.out.println("線程1釋放鎖");
            }
        });

        thread1.start();
        Thread.sleep(500); // 確保線程1先獲取到鎖

        Thread thread2 = new Thread(() -> {
            System.out.println("線程2嘗試獲取鎖...");
            try {
                // 可中斷的獲取鎖
                lock.lockInterruptibly();
                System.out.println("線程2獲取到鎖");
            } catch (InterruptedException e) {
                System.out.println("線程2等待鎖的過程被中斷了");
                // 恢復中斷狀態
                Thread.currentThread().interrupt();
            }
        });

        thread2.start();
        Thread.sleep(1000); // 給線程2一些時間嘗試獲取鎖

        // 中斷線程2的等待
        System.out.println("主線程決定中斷線程2的等待");
        thread2.interrupt();

        // 等待線程2處理完中斷
        thread2.join();
        System.out.println("程序結束");
    }
}

輸出結果:

線程1獲取到鎖,將無限期持有...
線程2嘗試獲取鎖...
主線程決定中斷線程2的等待
線程2等待鎖的過程被中斷了
程序結束

這個案例説明:使用lockInterruptibly()可以避免線程無限期地等待鎖,增強了程序的可控性。相比之下,如果使用lock()方法,線程 2 將無法響應中斷,只能一直等待。

3.2 案例 2:使用 tryLock 實現超時等待

在一些對時間敏感的系統中,無限期等待鎖可能導致嚴重問題。使用tryLock()方法可以設置等待超時時間:

public class TryLockDemo {
    private static final ReentrantLock lock = new ReentrantLock();

    public static void main(String[] args) {
        Thread thread1 = new Thread(() -> {
            try {
                lock.lock();
                System.out.println("線程1獲取到鎖");
                // 模擬持有鎖的工作
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
                // 恢復中斷狀態
                Thread.currentThread().interrupt();
            } finally {
                lock.unlock();
                System.out.println("線程1釋放鎖");
            }
        });

        thread1.start();

        // 確保線程1先獲取到鎖
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
            // 恢復中斷狀態
            Thread.currentThread().interrupt();
        }

        Thread thread2 = new Thread(() -> {
            boolean acquired = false;
            try {
                System.out.println("線程2嘗試獲取鎖,最多等待2秒");
                // 嘗試在2秒內獲取鎖
                acquired = lock.tryLock(2, TimeUnit.SECONDS);
                if (acquired) {
                    System.out.println("線程2成功獲取到鎖");
                    // 模擬工作
                    Thread.sleep(1000);
                } else {
                    System.out.println("線程2獲取鎖失敗,執行備選方案");
                    // 執行其他操作...
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
                // 重要:恢復中斷狀態,以便調用者能夠檢測到中斷
                Thread.currentThread().interrupt();
            } finally {
                if (acquired) {
                    lock.unlock();
                    System.out.println("線程2釋放鎖");
                }
            }
        });

        thread2.start();
    }
}

注意上面代碼中,當捕獲InterruptedException時,我們調用了Thread.currentThread().interrupt()來恢復線程的中斷狀態。這是因為異常被捕獲後,線程的中斷狀態會被清除,而恢復中斷狀態可以讓上層調用者知道線程曾經被中斷過。

輸出結果:

線程1獲取到鎖
線程2嘗試獲取鎖,最多等待2秒
線程2獲取鎖失敗,執行備選方案
線程1釋放鎖

這個案例演示瞭如何避免線程長時間等待,提高系統的響應性。tryLock方法在分佈式系統或微服務架構中特別有用,可以防止級聯阻塞。

3.3 案例 3:使用 Condition 實現精準線程通信

使用 Condition 可以實現更精細的線程控制,下面是一個使用多個 Condition 實現的有界緩衝區示例,並演示了 Condition 的超時等待特性:

public class BoundedBuffer {
    private final ReentrantLock lock = new ReentrantLock();
    private final Condition notFull = lock.newCondition();   // 緩衝區不滿條件
    private final Condition notEmpty = lock.newCondition();  // 緩衝區不空條件

    private final Object[] items;
    private int putIndex, takeIndex, count;

    public BoundedBuffer(int capacity) {
        items = new Object[capacity];
    }

    // 存入數據
    public void put(Object x) throws InterruptedException {
        lock.lock();
        try {
            // 使用while循環檢查條件,防止虛假喚醒
            while (count == items.length) {
                System.out.println(Thread.currentThread().getName() + " 發現緩衝區已滿,等待...");
                notFull.await();  // 必須在持有鎖的狀態下調用
            }

            items[putIndex] = x;
            if (++putIndex == items.length) putIndex = 0;
            ++count;

            System.out.println(Thread.currentThread().getName() + " 放入數據: " + x +
                             ",當前緩衝區數據量: " + count);

            // 通知消費者可以取數據了
            notEmpty.signal();  // 精確通知等待緩衝區不空的線程
        } finally {
            lock.unlock();
        }
    }

    // 取出數據(帶超時)
    public Object takeWithTimeout(long timeout, TimeUnit unit) throws InterruptedException {
        lock.lock();
        try {
            // 計算截止時間
            long nanos = unit.toNanos(timeout);

            // 使用while循環檢查條件
            while (count == 0) {
                System.out.println(Thread.currentThread().getName() + " 發現緩衝區為空,嘗試等待" +
                                  timeout + unit.toString().toLowerCase() + "...");

                if (nanos <= 0) {
                    // 超時退出
                    System.out.println(Thread.currentThread().getName() + " 等待超時,返回null");
                    return null;
                }

                // 帶超時的等待,返回剩餘等待時間
                nanos = notEmpty.awaitNanos(nanos);
            }

            Object x = items[takeIndex];
            if (++takeIndex == items.length) takeIndex = 0;
            --count;

            System.out.println(Thread.currentThread().getName() + " 取出數據: " + x +
                             ",當前緩衝區數據量: " + count);

            // 通知生產者可以放數據了
            notFull.signal();  // 精確通知等待緩衝區不滿的線程
            return x;
        } finally {
            lock.unlock();
        }
    }

    // 喚醒所有等待的生產者(示例signalAll()用法)
    public void signalAllProducers() {
        lock.lock();
        try {
            System.out.println("喚醒所有等待的生產者線程");
            notFull.signalAll();  // 喚醒所有等待"不滿"條件的線程
        } finally {
            lock.unlock();
        }
    }

    // 原始的取出方法
    public Object take() throws InterruptedException {
        lock.lock();
        try {
            while (count == 0) {
                System.out.println(Thread.currentThread().getName() + " 發現緩衝區為空,等待...");
                notEmpty.await();
            }

            Object x = items[takeIndex];
            if (++takeIndex == items.length) takeIndex = 0;
            --count;

            System.out.println(Thread.currentThread().getName() + " 取出數據: " + x +
                             ",當前緩衝區數據量: " + count);

            notFull.signal();
            return x;
        } finally {
            lock.unlock();
        }
    }

    public static void main(String[] args) {
        BoundedBuffer buffer = new BoundedBuffer(3);

        // 生產者線程(速度較慢)
        Thread producer = new Thread(() -> {
            try {
                for (int i = 1; i <= 5; i++) {
                    Thread.sleep(500);  // 生產慢一點,讓消費者體驗超時
                    buffer.put(i);
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
                Thread.currentThread().interrupt();
            }
        }, "生產者");

        // 消費者線程(帶超時)
        Thread consumer = new Thread(() -> {
            try {
                for (int i = 1; i <= 10; i++) {
                    // 超時等待2秒
                    Object item = buffer.takeWithTimeout(2, TimeUnit.SECONDS);
                    if (item == null) {
                        System.out.println("消費者因超時放棄等待,循環次數: " + i);
                    }
                    Thread.sleep(100);  // 消費速度快一些
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
                Thread.currentThread().interrupt();
            }
        }, "消費者");

        consumer.start();  // 先啓動消費者,這樣必然會遇到空緩衝區
        try {
            Thread.sleep(1000);  // 讓消費者先等一會兒
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        producer.start();  // 後啓動生產者
    }
}

上面代碼中有幾個關鍵點需要特別注意:

  1. 使用 while 而非 if 檢查條件:這是防止虛假喚醒(Spurious Wakeup)。線程可能在沒有被顯式喚醒的情況下從await()返回,使用 while 循環確保條件確實滿足。
  2. await()signal()必須在持有鎖的情況下調用:這與synchronized中的wait()/notify()一樣,是線程安全的基本要求。
  3. 精確通知notFull.signal()只會喚醒等待"不滿"條件的生產者線程,notEmpty.signal()只會喚醒等待"不空"條件的消費者線程。這比synchronized中的notify()更有針對性。
  4. 超時等待takeWithTimeout方法展示瞭如何使用Condition.awaitNanos()實現帶超時的等待,避免了消費者無限期等待的問題。
  5. 信號類型選擇:示例中還展示了signalAll()方法的用法,當需要喚醒多個等待線程時(如清空緩衝區操作),應使用signalAll()而非signal()

四、ReentrantLock 底層原理探秘

ReentrantLock 的強大功能離不開其底層實現機制——AQS(AbstractQueuedSynchronizer)。

graph TD
    A[ReentrantLock] --> B[AbstractQueuedSynchronizer AQS]
    B --> C[volatile int state]
    B --> D[FIFO雙向等待隊列]
    C --> E[state=0 表示無鎖]
    C --> F[state>0 表示有鎖]
    F --> G[state值=持有線程重入次數]
    D --> H[節點狀態CANCELLED/SIGNAL等]

AQS 內部維護了一個 volatile 變量 state 和一個 FIFO 的等待隊列。對於 ReentrantLock:

  • state = 0 表示鎖空閒
  • state > 0 表示鎖被佔用,值記錄了重入次數
  • 當一個線程獲取鎖失敗時,它會被包裝成一個 Node 加入 FIFO 隊列
  • 隊列中的節點有不同狀態(如 CANCELLED、SIGNAL 等),AQS 通過這些狀態管理線程的阻塞與喚醒,避免無效競爭
  • 釋放鎖時會喚醒隊列中的後繼節點

在非公平鎖實現中,新到來的線程可以直接嘗試 CAS 獲取鎖,而不必排隊;在公平鎖實現中,線程必須先檢查隊列中是否有前驅節點,只有沒有前驅時才能嘗試獲取鎖。

這種機制使得 ReentrantLock 能夠高效地管理鎖競爭,並支持公平或非公平獲取鎖的策略。

五、ReentrantLock 使用注意事項

5.1 必須手動釋放鎖

與 synchronized 不同,ReentrantLock 要求手動釋放鎖,通常的模式是:

ReentrantLock lock = new ReentrantLock();
lock.lock();  // 獲取鎖
try {
    // 臨界區代碼
} finally {
    lock.unlock();  // 確保鎖被釋放
}

為什麼要放在 finally 塊中?
防止臨界區代碼拋出異常而導致鎖無法釋放,進而引發死鎖。這是使用 ReentrantLock 最容易出錯的地方,必須養成良好習慣。

5.2 公平鎖與非公平鎖的選擇

  • 非公平鎖(默認):吞吐量更高,但可能造成線程飢餓
  • 公平鎖:等待更公平,但整體性能較低

根據 Oracle JDK 的官方基準測試,在高競爭環境下,公平鎖的吞吐量比非公平鎖低約 10%-20%。這是因為公平鎖需要維護一個嚴格的 FIFO 隊列,額外的檢查和同步開銷導致性能下降。

一般情況下使用默認的非公平鎖即可,除非系統特別需要保證每個線程的公平性。

5.3 性能考量

ReentrantLock 相比 synchronized 在不同場景下的性能表現:

  • 低競爭場景:JDK 1.6 後對 synchronized 進行了大量優化(偏向鎖、輕量級鎖),在低競爭情況下,synchronized 性能接近甚至優於 ReentrantLock
  • 高競爭場景:ReentrantLock 的靈活性(如超時獲取、可中斷)和精確的線程控制能帶來更好的整體性能

選擇時應考慮實際應用場景和鎖競爭的激烈程度。

六、ReentrantLock vs synchronized

來看看它們的主要區別:

特性 ReentrantLock synchronized
鎖獲取方式 顯式(lock()) 隱式(進入同步塊)
鎖釋放方式 顯式(unlock()) 隱式(離開同步塊)
鎖類型 接口實現,可以繼承 關鍵字,內置語言特性
可中斷獲取 支持(lockInterruptibly()) 不支持
超時獲取 支持(tryLock(time)) 不支持
公平性 可選擇(默認非公平) 非公平
多條件變量 支持(Condition) 不支持(只有一個等待集合)
性能(低競爭) 較好 JDK 1.6 優化後較好
性能(高競爭) 較好 JDK 1.6 優化後接近
鎖狀態檢查 支持(isLocked()等) 不支持
編碼複雜度 較高(需手動解鎖) 較低(自動解鎖)

七、ReentrantLock 進階案例:可重入讀寫鎖

在某些場景下,我們需要區分讀操作和寫操作的鎖定粒度。ReentrantReadWriteLock 提供了這種能力:

public class ReadWriteLockDemo {
    private final ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
    private final Lock readLock = rwLock.readLock();
    private final Lock writeLock = rwLock.writeLock();

    private final Map<String, String> data = new HashMap<>();

    // 寫操作:獨佔鎖
    public void put(String key, String value) {
        writeLock.lock();
        try {
            System.out.println(Thread.currentThread().getName() + " 正在寫入數據...");
            // 模擬寫入耗時
            Thread.sleep(1000);
            data.put(key, value);
            System.out.println(Thread.currentThread().getName() + " 寫入完成: " + key + "=" + value);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            writeLock.unlock();
        }
    }

    // 讀操作:共享鎖
    public String get(String key) {
        readLock.lock();
        try {
            System.out.println(Thread.currentThread().getName() + " 正在讀取數據...");
            // 模擬讀取耗時
            Thread.sleep(200);  // 讀操作比寫操作快,更能體現讀共享優勢
            String value = data.get(key);
            System.out.println(Thread.currentThread().getName() + " 讀取完成: " + key + "=" + value);
            return value;
        } catch (InterruptedException e) {
            e.printStackTrace();
            return null;
        } finally {
            readLock.unlock();
        }
    }

    public static void main(String[] args) throws InterruptedException {
        ReadWriteLockDemo demo = new ReadWriteLockDemo();

        // 預先放入一些數據
        demo.put("key1", "value1");

        // 創建10個讀線程,更好地展示讀併發效果
        for (int i = 0; i < 10; i++) {
            final int index = i;
            new Thread(() -> {
                demo.get("key1");
            }, "讀線程" + index).start();
        }

        // 創建2個寫線程
        for (int i = 0; i < 2; i++) {
            final int index = i;
            new Thread(() -> {
                demo.put("key" + (index + 2), "value" + (index + 2));
            }, "寫線程" + index).start();
        }
    }
}

關鍵點:

  • 寫鎖是獨佔的:一次只能有一個線程獲取寫鎖
  • 讀鎖是共享的:多個線程可以同時獲取讀鎖
  • 寫鎖和讀鎖互斥:有寫鎖時不能獲取讀鎖,有讀鎖時不能獲取寫鎖
  • 適合"讀多寫少"的場景

7.1 鎖降級:從寫鎖降級為讀鎖

一個重要但常被忽略的技巧是鎖降級,即持有寫鎖的線程可以獲取讀鎖,然後釋放寫鎖,這樣就從寫鎖降級為讀鎖了:

public class LockDegradingDemo {
    private final ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
    private final Lock readLock = rwLock.readLock();
    private final Lock writeLock = rwLock.writeLock();

    private Map<String, Object> cacheMap = new HashMap<>();

    // 使用鎖降級更新緩存
    public Object processCachedData(String key) {
        Object value = null;

        // 首先獲取讀鎖查詢緩存
        readLock.lock();
        try {
            value = cacheMap.get(key);
            if (value == null) {
                // 緩存未命中,釋放讀鎖,獲取寫鎖
                readLock.unlock();
                writeLock.lock();
                try {
                    // 再次檢查,因為可能其他線程已經更新了緩存
                    value = cacheMap.get(key);
                    if (value == null) {
                        // 模擬從數據庫加載數據
                        value = loadFromDatabase(key);
                        cacheMap.put(key, value);
                        System.out.println("緩存更新完畢: " + key);
                    }

                    // 鎖降級:持有寫鎖的同時獲取讀鎖
                    readLock.lock();
                } finally {
                    // 釋放寫鎖,保留讀鎖
                    writeLock.unlock();
                }
                // 此時線程仍持有讀鎖
            }

            // 使用讀鎖保護的數據
            return value;
        } finally {
            readLock.unlock();
        }
    }

    private Object loadFromDatabase(String key) {
        System.out.println("從數據庫加載: " + key);
        // 模擬耗時操作
        try {
            Thread.sleep(500);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return "DB_" + key + "_VALUE";
    }

    public static void main(String[] args) {
        LockDegradingDemo demo = new LockDegradingDemo();

        // 多線程併發訪問
        for (int i = 0; i < 5; i++) {
            final String key = "key" + (i % 2);  // 只使用兩個不同的key,增加併發更新的可能
            new Thread(() -> {
                Object value = demo.processCachedData(key);
                System.out.println(Thread.currentThread().getName() + " 獲取到: " + key + "=" + value);
            }, "Thread-" + i).start();
        }
    }
}

鎖降級的好處是保證數據的可見性。在更新完數據後,如果我們先釋放寫鎖再獲取讀鎖,那麼在這個短暫的時間窗口內,可能有其他線程修改了數據。通過鎖降級,我們確保讀取的是自己最新寫入的數據。

八、總結

通過本文的講解,我們全面瞭解了 ReentrantLock 的高級特性與應用。下表總結了 ReentrantLock 的關鍵特性和應用場景:

特性 方法 適用場景 注意事項
基本鎖獲取 lock() 一般同步場景 必須在 finally 中解鎖
可重入性 內置特性 遞歸調用、嵌套鎖 調用 unlock 次數必須等於 lock 次數
嘗試獲取鎖 tryLock() 避免死鎖、提高響應性 結果為 false 時需有備選方案
可中斷鎖獲取 lockInterruptibly() 需要中斷能力的場景 拋出 InterruptedException 後恢復中斷狀態
超時鎖獲取 tryLock(time, unit) 限時等待場景 超時返回 false
公平性控制 構造函數參數 需要減少飢餓的場景 公平鎖性能約低 10%-20%
條件變量 newCondition() 複雜線程協作 await 前必須持有鎖,使用 while 循環檢查條件
超時等待 await(time, unit) 需限時等待的場景 返回值表示是否超時
鎖狀態查詢 isLocked()等 調試和監控 結果可能立即過時
讀寫鎖分離 ReentrantReadWriteLock 讀多寫少的場景 寫鎖可降級為讀鎖,反之不可

最後,記住一條黃金法則:鎖的範圍要儘可能小,持有時間要儘可能短。這樣能最大限度地減少線程間的競爭,提高程序的併發性能。

在實際項目中,根據業務需求的不同,靈活選擇合適的鎖機制,才能構建高效、穩定的多線程應用!

在下一篇文章中,我們將探討“線程間通信的三種經典方式”,敬請期待!


感謝您耐心閲讀到這裏!如果覺得本文對您有幫助,歡迎點贊 👍、收藏 ⭐、分享給需要的朋友,您的支持是我持續輸出技術乾貨的最大動力!

如果想獲取更多 Java 技術深度解析,歡迎點擊頭像關注我,後續會每日更新高質量技術文章,陪您一起進階成長~

user avatar
0 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.