一、鎖的進化:從 synchronized 到 ReentrantLock
大家好,在多線程編程中,鎖機制是保證線程安全的核心技術。Java 早期只提供了 synchronized 這一種內置鎖,而在 JDK 1.5 後,Doug Lea 大師為我們帶來了更加靈活強大的顯式鎖ReentrantLock。
synchronized 雖然用起來簡單,但在某些場景下會顯得"能力不足":
- 無法響應中斷請求
- 無法嘗試獲取鎖
- 不支持公平性選擇
- 通知機制基於單一等待隊列,難以實現精準喚醒
這時,ReentrantLock就成了我們的"救星"。讓我們一起來深入瞭解這把鎖!
二、ReentrantLock 的核心特性
ReentrantLock 是 Lock 接口的一個實現,它提供了比 synchronized 更豐富的功能:
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 提供了兩種獲取鎖的方式:公平鎖和非公平鎖。
- 公平鎖:嚴格按照線程請求的順序獲取鎖,類似於排隊買票,先來先得
- 非公平鎖:不保證等待時間最長的線程優先獲取鎖,允許"插隊",默認模式
創建方式對比:
// 默認創建非公平鎖
ReentrantLock unfairLock = new ReentrantLock();
// 創建公平鎖
ReentrantLock fairLock = new ReentrantLock(true);
公平鎖的優點是顯著降低了"飢餓"現象發生的概率,保證每個線程都有機會獲取鎖;缺點是整體吞吐量相對較低。非公平鎖則允許更充分地利用 CPU 資源,但可能導致某些線程長時間等待。
需要注意的是,即使使用公平鎖,也無法完全杜絕飢餓現象,因為線程可能因為其他原因(如中斷或取消)退出等待隊列。
場景選擇建議:
- 在高併發且線程生命週期較短的場景中,非公平鎖通常表現更好,因為新線程可以立即嘗試獲取鎖,減少上下文切換
- 在線程任務執行時間差異大、並且某些線程優先級較低的系統中,公平鎖可以減少低優先級線程的飢餓概率
- 對於需要嚴格保證請求順序的系統(如排隊系統),公平鎖是更合適的選擇
2.3 多種獲取鎖的方式
ReentrantLock 提供了多種獲取鎖的方式,大大增強了靈活性:
- lock():最基本的獲取鎖方法,如果鎖被佔用,會一直等待
- tryLock():嘗試獲取鎖,立即返回結果(成功/失敗),不會阻塞
- tryLock(long timeout, TimeUnit unit):在指定時間內嘗試獲取鎖
- lockInterruptibly():可中斷的獲取鎖,允許在等待時響應中斷信號
我們可以用一個餐廳排隊的例子來理解:
lock():不管多久我都要等到有位置tryLock():看一眼有沒有空位,有就坐,沒有就走tryLock(time):最多等 30 分鐘,如果還沒位置就去別家lockInterruptibly():等位過程中如果接到重要電話可以中途離開
2.4 精準通知機制:Condition
ReentrantLock 結合 Condition 接口,提供了比 synchronized + wait/notify 更加強大的線程通信能力:
與 synchronized 相比的優勢:
- 一個鎖可以創建多個 Condition 對象,實現"選擇性通知"
- 更精準的線程控制,避免了 Object.notify()的盲目喚醒
- 提供帶超時的等待和可中斷的等待
信號類型對比:
- signal():只喚醒單個等待該條件的線程,適用於只需要喚醒一個消費者/生產者的場景
- signalAll():喚醒所有等待該條件的線程,適用於需要通知所有相關線程的狀態變更場景
重要提示:Condition的await()和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(); // 後啓動生產者
}
}
上面代碼中有幾個關鍵點需要特別注意:
- 使用 while 而非 if 檢查條件:這是防止虛假喚醒(Spurious Wakeup)。線程可能在沒有被顯式喚醒的情況下從
await()返回,使用 while 循環確保條件確實滿足。 await()和signal()必須在持有鎖的情況下調用:這與synchronized中的wait()/notify()一樣,是線程安全的基本要求。- 精確通知:
notFull.signal()只會喚醒等待"不滿"條件的生產者線程,notEmpty.signal()只會喚醒等待"不空"條件的消費者線程。這比synchronized中的notify()更有針對性。 - 超時等待:
takeWithTimeout方法展示瞭如何使用Condition.awaitNanos()實現帶超時的等待,避免了消費者無限期等待的問題。 - 信號類型選擇:示例中還展示了
signalAll()方法的用法,當需要喚醒多個等待線程時(如清空緩衝區操作),應使用signalAll()而非signal()。
四、ReentrantLock 底層原理探秘
ReentrantLock 的強大功能離不開其底層實現機制——AQS(AbstractQueuedSynchronizer)。
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 技術深度解析,歡迎點擊頭像關注我,後續會每日更新高質量技術文章,陪您一起進階成長~