在 Java 并發編程中,除了synchronized關鍵字,java.util.concurrent.locks.Lock接口及其實現類是另一種重要的同步機制。自 JDK 5 引入以來,Lock接口憑借靈活的 API 設計、可中斷的鎖獲取、公平性控制等特性,成為復雜并發場景的首選方案。本文將從Lock接口的核心方法入手,深入解析ReentrantLock、ReentrantReadWriteLock等實現類的工作原理,對比其與synchronized的差異,并通過實戰案例展示如何在實際開發中正確使用。
一、Lock 接口:同步機制的抽象定義
Lock接口是 Java 并發包對鎖機制的抽象,它將鎖的獲取與釋放等操作封裝為顯式方法,相比synchronized的隱式操作,提供了更高的靈活性。
1.1 核心方法解析
Lock接口的核心方法定義了鎖的基本操作,理解這些方法是使用Lock的基礎:
方法 | 功能描述 | 關鍵特性 |
void lock() | 獲取鎖,若鎖被占用則阻塞 | 不可中斷,與synchronized類似 |
void lockInterruptibly() throws InterruptedException | 獲取鎖,可響應中斷 | 允許線程在等待鎖時被中斷(如Thread.interrupt()) |
boolean tryLock() | 嘗試獲取鎖,立即返回結果 | 非阻塞,成功返回true,失敗返回false |
boolean tryLock(long time, TimeUnit unit) throws InterruptedException | 超時嘗試獲取鎖 | 結合了超時等待與可中斷特性 |
void unlock() | 釋放鎖 | 必須在finally塊中調用,避免鎖泄漏 |
Condition newCondition() | 創建條件變量 | 用于線程間的協作通信 |
核心設計思想:Lock接口將鎖的 “獲取” 與 “釋放” 解耦為獨立方法,開發者需手動控制這兩個操作,這既帶來了靈活性,也要求更嚴謹的編碼(如必須在finally中釋放鎖)。
1.2 與 synchronized 的本質區別
Lock接口與synchronized的核心差異體現在控制粒度和功能擴展上:
- 獲取與釋放的顯式性:synchronized的鎖獲取與釋放是隱式的(進入代碼塊自動獲取,退出自動釋放),而Lock需要手動調用lock()和unlock();
- 靈活性:Lock支持中斷、超時、公平性設置等,而synchronized僅支持最基本的互斥;
- 底層實現:synchronized是 JVM 層面的實現(依賴 C++ 代碼),Lock是 Java 代碼層面的實現(基于 AQS 框架)。
二、ReentrantLock:可重入鎖的經典實現
ReentrantLock是Lock接口最常用的實現類,其名稱中的 “Reentrant” 表示可重入性—— 即線程可以多次獲取同一把鎖,這與synchronized的特性一致。
2.1 基本使用方法
ReentrantLock的使用遵循 “獲取 - 使用 - 釋放” 的模式,釋放操作必須放在finally塊中,確保鎖在任何情況下都能被釋放:
public class ReentrantLockDemo {private final Lock lock = new ReentrantLock(); // 創建ReentrantLock實例private int count = 0;public void increment() {lock.lock(); // 獲取鎖try {count++; // 臨界區操作} finally {lock.unlock(); // 釋放鎖,必須在finally中執行}}public int getCount() {lock.lock();try {return count;} finally {lock.unlock();}}
}
注意事項:
- 若忘記調用unlock(),會導致鎖永久持有,其他線程無法獲取,造成死鎖;
- 同一線程多次調用lock()后,必須調用相同次數的unlock()才能完全釋放鎖(可重入特性)。
2.2 核心特性詳解
2.2.1 可重入性
ReentrantLock允許線程重復獲取鎖,獲取次數與釋放次數必須一致:
public class ReentrantDemo {private static final Lock lock = new ReentrantLock();public static void main(String[] args) {lock.lock();try {System.out.println("第一次獲取鎖");lock.lock(); // 再次獲取鎖(可重入)try {System.out.println("第二次獲取鎖");} finally {lock.unlock(); // 第二次釋放}} finally {lock.unlock(); // 第一次釋放}}
}
實現原理:ReentrantLock內部通過計數器記錄線程獲取鎖的次數,每次lock()計數器加 1,unlock()計數器減 1,當計數器為 0 時,鎖才真正釋放。
2.2.2 公平性控制
ReentrantLock支持公平鎖與非公平鎖兩種模式,通過構造函數指定:
// 非公平鎖(默認):線程獲取鎖的順序不保證與請求順序一致,可能存在插隊
Lock nonFairLock = new ReentrantLock();// 公平鎖:線程獲取鎖的順序與請求順序一致,先請求的線程先獲取
Lock fairLock = new ReentrantLock(true);
公平性的權衡:
- 公平鎖:避免線程饑餓(某些線程長期無法獲取鎖),但性能較差(需要維護等待隊列的順序);
- 非公平鎖:性能更好(允許插隊,減少線程切換開銷),但可能導致某些線程長時間等待。
適用場景:
- 對公平性要求高的場景(如資源調度系統)使用公平鎖;
- 追求高性能的一般場景使用非公平鎖(默認)。
2.2.3 可中斷的鎖獲取
lockInterruptibly()方法允許線程在等待鎖的過程中響應中斷,避免無限期阻塞:
public class InterruptibleLockDemo {private static final Lock lock = new ReentrantLock();public static void main(String[] args) throws InterruptedException {Thread t1 = new Thread(() -> {try {lock.lockInterruptibly(); // 可中斷地獲取鎖try {Thread.sleep(1000); // 模擬耗時操作} finally {lock.unlock();}} catch (InterruptedException e) {System.out.println("線程1被中斷,放棄獲取鎖");}});lock.lock(); // 主線程先獲取鎖t1.start();Thread.sleep(200);t1.interrupt(); // 中斷線程1的等待lock.unlock(); // 釋放主線程的鎖}
}
運行結果:線程 1 在等待鎖時被中斷,執行catch塊邏輯,避免永久阻塞。
2.2.4 超時獲取鎖
tryLock(long time, TimeUnit unit)方法允許線程在指定時間內嘗試獲取鎖,超時未獲取則返回false:
public class TimeoutLockDemo {private static final Lock lock = new ReentrantLock();public static void main(String[] args) throws InterruptedException {Thread t1 = new Thread(() -> {try {// 嘗試在1秒內獲取鎖if (lock.tryLock(1, TimeUnit.SECONDS)) {try {System.out.println("線程1獲取到鎖");Thread.sleep(2000); // 持有鎖2秒} finally {lock.unlock();}} else {System.out.println("線程1超時未獲取到鎖");}} catch (InterruptedException e) {e.printStackTrace();}});lock.lock();t1.start();Thread.sleep(1500); // 主線程持有鎖1.5秒lock.unlock();}
}
運行結果:線程 1 等待 1 秒后仍未獲取鎖,輸出 “超時未獲取到鎖”(主線程 1.5 秒后才釋放鎖)。
2.3 條件變量(Condition)的使用
ReentrantLock通過newCondition()方法創建Condition對象,實現線程間的靈活通信,相比synchronized的wait()/notify(),Condition支持多條件等待。
示例:生產者 - 消費者模式
public class ConditionDemo {private final Lock lock = new ReentrantLock();private final Condition notEmpty = lock.newCondition(); // 非空條件private final Condition notFull = lock.newCondition(); // 非滿條件private final Queue<Integer> queue = new LinkedList<>();private static final int CAPACITY = 5;// 生產者public void put(int value) throws InterruptedException {lock.lock();try {// 隊列滿則等待while (queue.size() == CAPACITY) {notFull.await(); // 等待非滿條件}queue.add(value);System.out.println("生產:" + value + ",隊列大小:" + queue.size());notEmpty.signal(); // 喚醒等待非空條件的線程} finally {lock.unlock();}}// 消費者public int take() throws InterruptedException {lock.lock();try {// 隊列空則等待while (queue.isEmpty()) {notEmpty.await(); // 等待非空條件}int value = queue.poll();System.out.println("消費:" + value + ",隊列大小:" + queue.size());notFull.signal(); // 喚醒等待非滿條件的線程return value;} finally {lock.unlock();}}
}
優勢:Condition將不同的等待條件分離(如 “隊列滿” 和 “隊列空”),避免了synchronized中notifyAll()喚醒所有線程導致的效率問題。
三、ReentrantReadWriteLock:讀寫分離的鎖機制
在多線程場景中,讀操作往往可以并發執行(無線程安全問題),而寫操作需要獨占訪問。ReentrantReadWriteLock通過分離讀鎖與寫鎖,實現 “讀多寫少” 場景下的性能優化。
3.1 核心特性
- 讀寫分離:包含ReadLock(讀鎖)和WriteLock(寫鎖),讀鎖可被多個線程同時持有,寫鎖是獨占的;
- 可重入性:讀鎖和寫鎖都支持重入;
- 降級支持:寫鎖可降級為讀鎖(先獲取寫鎖,再獲取讀鎖,最后釋放寫鎖),但讀鎖不能升級為寫鎖。
鎖的兼容性規則:
當前持有鎖 | 新請求的鎖 | 能否獲取 |
無鎖 | 讀鎖 | 能(多個線程可同時獲取) |
無鎖 | 寫鎖 | 能(獨占) |
讀鎖 | 讀鎖 | 能(共享) |
讀鎖 | 寫鎖 | 不能(寫鎖需獨占,等待所有讀鎖釋放) |
寫鎖 | 讀鎖 | 能(同一線程可獲取,實現鎖降級) |
寫鎖 | 寫鎖 | 能(同一線程可重入,其他線程不能) |
3.2 使用方法
ReentrantReadWriteLock的使用需分別獲取讀鎖和寫鎖:
public class ReadWriteLockDemo {private final ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();private final Lock readLock = rwLock.readLock(); // 讀鎖private final Lock writeLock = rwLock.writeLock(); // 寫鎖private Map<String, Object> cache = new HashMap<>();// 讀操作:使用讀鎖public Object get(String key) {readLock.lock();try {System.out.println("讀取key:" + key + ",當前線程數:" + rwLock.getReadLockCount());return cache.get(key);} finally {readLock.unlock();}}// 寫操作:使用寫鎖public void put(String key, Object value) {writeLock.lock();try {System.out.println("寫入key:" + key);cache.put(key, value);} finally {writeLock.unlock();}}
}
性能優勢:在高并發讀場景下,ReentrantReadWriteLock的吞吐量遠高于synchronized或ReentrantLock(讀操作無需互斥)。
3.3 鎖降級示例
鎖降級是指寫鎖持有者先獲取讀鎖,再釋放寫鎖,確保后續讀操作的原子性:
public void downgradeLock() {writeLock.lock();try {System.out.println("獲取寫鎖,準備更新數據");// 更新數據...readLock.lock(); // 降級:獲取讀鎖System.out.println("獲取讀鎖,完成降級");} finally {writeLock.unlock(); // 釋放寫鎖,保留讀鎖}try {// 持有讀鎖進行后續操作System.out.println("持有讀鎖,讀取數據");} finally {readLock.unlock(); // 最終釋放讀鎖}
}
用途:鎖降級確保寫操作完成后,讀操作能立即看到最新數據,且不會被其他寫操作中斷。
四、Lock 與 synchronized 的全面對比及選擇指南
4.1 功能對比
特性 | Lock(以 ReentrantLock 為例) | synchronized |
可重入性 | 支持 | 支持 |
公平性 | 可設置公平 / 非公平 | 僅非公平 |
鎖獲取方式 | 顯式(lock()/unlock()) | 隱式(代碼塊 / 方法) |
可中斷性 | 支持(lockInterruptibly()) | 不支持 |
超時獲取 | 支持(tryLock(time)) | 不支持 |
條件變量 | 支持多條件(Condition) | 僅單條件(wait()/notify()) |
性能 | 高競爭場景下更優 | 低競爭場景下接近Lock |
靈活性 | 高(可自定義擴展) | 低(固定實現) |
4.2 適用場景選擇
- 優先使用 synchronized 的場景:
- 簡單的同步代碼塊或方法(語法簡潔,不易出錯);
- 無復雜需求(如中斷、超時)的場景;
- 單線程或低并發場景(性能差異可忽略)。
- 優先使用 ReentrantLock 的場景:
- 需要中斷等待鎖的線程(如取消任務);
- 需要超時獲取鎖避免死鎖;
- 需要多條件變量進行線程通信;
- 需要公平鎖保證線程調度順序。
- 優先使用 ReentrantReadWriteLock 的場景:
- 讀操作遠多于寫操作的場景(如緩存、配置讀取);
- 需要讀寫分離提高并發讀性能。
五、實戰案例:用 ReentrantLock 解決死鎖問題
場景:兩個線程分別需要獲取兩把鎖,但獲取順序相反,使用synchronized會導致死鎖,而Lock的tryLock()可避免。
解決方案:
public class DeadlockSolution {private final Lock lockA = new ReentrantLock();private final Lock lockB = new ReentrantLock();// 線程1的操作:先獲取lockA,再獲取lockBpublic void operation1() throws InterruptedException {if (lockA.tryLock(1, TimeUnit.SECONDS)) { // 超時嘗試獲取lockAtry {Thread.sleep(100); // 模擬操作if (lockB.tryLock(1, TimeUnit.SECONDS)) { // 超時嘗試獲取lockBtry {System.out.println("線程1獲取到兩把鎖,執行操作");} finally {lockB.unlock();}} else {System.out.println("線程1獲取lockB超時,釋放lockA");}} finally {lockA.unlock();}} else {System.out.println("線程1獲取lockA超時,放棄操作");}}// 線程2的操作:先獲取lockB,再獲取lockApublic void operation2() throws InterruptedException {if (lockB.tryLock(1, TimeUnit.SECONDS)) { // 超時嘗試獲取lockBtry {Thread.sleep(100); // 模擬操作if (lockA.tryLock(1, TimeUnit.SECONDS)) { // 超時嘗試獲取lockAtry {System.out.println("線程2獲取到兩把鎖,執行操作");} finally {lockA.unlock();}} else {System.out.println("線程2獲取lockA超時,釋放lockB");}} finally {lockB.unlock();}} else {System.out.println("線程2獲取lockB超時,放棄操作");}}public static void main(String[] args) throws InterruptedException {DeadlockSolution solution = new DeadlockSolution();// 啟動線程1執行operation1Thread t1 = new Thread(() -> {try {solution.operation1();} catch (InterruptedException e) {e.printStackTrace();}});// 啟動線程2執行operation2Thread t2 = new Thread(() -> {try {solution.operation2();} catch (InterruptedException e) {e.printStackTrace();}});t1.start();t2.start();t1.join();t2.join();System.out.println("操作完成");}
}
運行結果解析:
- 線程 1 和線程 2 分別嘗試獲取對方已持有的鎖時,會因超時機制釋放已獲取的鎖,避免死鎖;
- 輸出可能為 “線程 1 獲取 lockB 超時,釋放 lockA” 和 “線程 2 獲取到兩把鎖,執行操作”,或反之,具體取決于線程調度,但絕不會出現死鎖。
核心原理:tryLock()的超時機制確保線程不會無限期等待鎖,當獲取鎖失敗時,會釋放已持有的鎖資源,打破死鎖的循環等待條件。
六、總結:Lock 接口的價值與最佳實踐
Lock接口及其實現類為 Java 并發編程提供了更靈活、更高效的同步選擇。無論是ReentrantReadWriteLock的讀寫分離,還是tryLock()的超時與中斷支持,都彌補了synchronized在復雜場景下的不足。
最佳實踐原則:
- 當讀操作遠多于寫操作時,優先使用ReentrantReadWriteLock提升并發性能;
- 當需要中斷等待鎖的線程或設置超時時間時,必須使用Lock接口;
- 始終在finally塊中釋放鎖,避免鎖泄漏;
- 簡單場景下,synchronized仍是更簡潔、更不易出錯的選擇。
理解Lock接口的設計思想,不僅能幫助我們寫出更高效的并發代碼,更能深入掌握 Java 并發編程的核心原理,為應對復雜場景打下堅實基礎。