ReentrantLock 使用
1. 前言
本節內容主要是對 ReentrantLock 的使用進行講解,之前對于 Lock 接口進行了講解,ReentrantLock 是 Lock 接口的常用實現子類,占據著十分重要的地位。本節內容的知識點如下:
- ReentrantLock 基本方法的使用,即 lock 與 unlock 方法的使用,這是最基礎的方法使用,為重點內容;
- ReentrantLock lockInterruptibly 與 tryLock 方法的使用,也是經常使用到的方法,為本節重點內容;
- ReentrantLock 公平鎖與非公平鎖的使用,也是本節的重點內容;
- ReentrantLock 其他方法的介紹與使用。
通篇來看,ReentrantLock 所有的知識點均為重點內容,是必須要掌握的內容。
2. ReentrantLock 介紹
ReentrantLock 在 Java 中也是一個基礎的鎖,ReentrantLock 實現 Lock 接口提供一系列的基礎函數,開發人員可以靈活的使用函數滿足各種復雜多變應用場景。
定義:ReentrantLock 是一個可重入且獨占式的鎖,它具有與使用 synchronized 監視器鎖相同的基本行為和語義,但與 synchronized 關鍵字相比,它更靈活、更強大,增加了輪詢、超時、中斷等高級功能。
ReentrantLock,顧名思義,它是支持可重入鎖的鎖,是一種遞歸無阻塞的同步機制。除此之外,該鎖還支持獲取鎖時的公平和非公平選擇。
公平性:ReentrantLock 的內部類 Sync 繼承了 AQS,分為公平鎖 FairSync 和非公平鎖 NonfairSync。
如果在絕對時間上,先對鎖進行獲取的請求一定先被滿足,那么這個鎖是公平的,反之,是不公平的。公平鎖的獲取,也就是等待時間最長的線程最優先獲取鎖,也可以說鎖獲取是順序的。
ReentrantLock 的公平與否,可以通過它的構造函數來決定。
3. ReentrantLock 基本方法 lock 與 unlock 的使用
我們使用一個之前涉及到的 synchronized 的場景,通過 lock 接口進行實現。
場景回顧:
- 創建兩個線程,創建方式可自選;
- 定義一個全局共享的 static int 變量 count,初始值為 0;
- 兩個線程同時操作 count,每次操作 count 加 1;
- 每個線程做 100 次 count 的增加操作。
結果預期:獲取到的結果為 200。之前我們使用了 synchronized 關鍵字和樂觀鎖 Amotic 操作進行了實現,那么此處我們進行 ReentrantLock 的實現方式。
實現步驟:
- step 1 :創建 ReentrantLock 實例,以便于調用 lock 方法和 unlock 方法;
- step 2:在 synchronized 的同步代碼塊處,將 synchronized 實現替換為 lock 實現。
實例:
public class DemoTest{
private static int count = 0; //定義count = 0
private static ReentrantLock lock = new ReentrantLock();//創建 lock 實例
public static void main(String[] args) {
for (int i = 0; i < 2; i++) { //通過for循環創建兩個線程
new Thread(new Runnable() {
@Override
public void run() {
//每個線程讓count自增100次
for (int i = 0; i < 100; i++) {
try {
lock.lock(); //調用 lock 方法
count++;
} finally {
lock.unlock(); //調用unlock方法釋放鎖
}
}
}
}). start();
}
try{
Thread.sleep(2000);
}catch (Exception e){
e.printStackTrace();
}
System.out.println(count);
}
}
代碼分析:
我們通過 try finally 模塊,替代了之前的 synchronized 代碼塊,順利的實現了多線程下的并發。
4. tryLock 方法
我們之前進行過介紹,Lock 接口包含了兩種 tryLock 方法,一種無參數,一種帶參數。
- boolean tryLock():僅在調用時鎖為空閑狀態才獲取該鎖。如果鎖可用,則獲取鎖,并立即返回值 true。如果鎖不可用,則此方法將立即返回值 false;
- boolean tryLock(long time, TimeUnit unit):如果鎖在給定的等待時間內空閑,并且當前線程未被中斷,則獲取鎖;
為了了解兩種方法的使用,我們先來設置一個簡單的使用場景。
場景設置:
- 創建兩個線程,創建方式自選;
- 兩個線程同時執行代碼邏輯;
- 代碼邏輯使用 boolean tryLock () 方法,如果獲取到鎖,執行打印當前線程名稱,并沉睡 5000 毫秒;如果未獲取鎖,則打印 timeout,并處理異常信息;
- 觀察結果并進行分析;
- 修改代碼,使用 boolean tryLock (long time, TimeUnit unit) 方法,設置時間為 4000 毫秒;
- 觀察結果并進行分析;
- 再次修改代碼,使用 boolean tryLock (long time, TimeUnit unit) 方法,設置時間為 6000 毫秒;
- 觀察結果并進行分析。
實例:使用 boolean tryLock () 方法
public class DemoTest implements Runnable{
private static Lock locks = new ReentrantLock();
@Override
public void run() {
try {
if(locks.tryLock()){ //嘗試獲取鎖,獲取成功則進入執行,不成功則執行finally模塊
System.out.println(Thread.currentThread().getName()+"-->");
Thread.sleep(5000);
}else{
System.out.println(Thread.currentThread().getName()+" time out ");
}
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
try {
locks.unlock();
} catch (Exception e) {
System.out.println(Thread.currentThread().getName() + "未獲取到鎖,釋放鎖拋出異常");
}
}
}
public static void main(String[] args) throws InterruptedException {
DemoTest test =new DemoTest();
Thread t1 = new Thread(test);
Thread t2 = new Thread(test);
t1. start();
t2. start();
t1.join();
t2.join();
System.out.println("over");
}
}
結果驗證:
Thread-1-->
Thread-0 time out
Thread-0 未獲取到鎖,釋放鎖拋出異常
over
結果分析:從打印的結果來看, Thread-1 獲取了鎖權限,而 Thread-0 沒有獲取鎖權限,這就是 tryLock,沒有獲取到鎖資源則放棄執行,直接調用 finally。
實例:使用 boolean tryLock (4000 ms) 方法
將 if 判斷進行修改如下:
if(locks.tryLock(4000,TimeUnit.MILLISECONDS)){ //嘗試獲取鎖,獲取成功則進入執行,不成功則執行finally模塊
System.out.println(Thread.currentThread().getName()+"-->");
Thread.sleep(5000);
}
結果驗證:
Thread-1-->
Thread-0 time out
Thread-0 未獲取到鎖,釋放鎖拋出異常
over
結果分析:tryLock 方法,雖然等待 4000 毫秒,但是這段時間不足以等待 Thread-1 釋放資源鎖,所以還是超時。 我們換成 6000 毫秒試試。
實例:使用 boolean tryLock (6000 ms) 方法
將 if 判斷進行修改如下:
if(locks.tryLock(6000,TimeUnit.MILLISECONDS)){ //嘗試獲取鎖,獲取成功則進入執行,不成功則執行finally模塊
System.out.println(Thread.currentThread().getName()+"-->");
Thread.sleep(5000);
}
結果驗證:
Thread-1-->
Thread-0-->
over
結果分析:tryLock 方法,等待 6000 毫秒,Thread-1 先進入執行,5000 毫秒后 Thread-0 進入執行,都能夠有機會獲取鎖。
總結:以上就是 tryLock 方法的使用,可以指定最長的獲取鎖的時間,如果獲取則執行,未獲取則放棄執行。
5. 公平鎖與非公平鎖
分類:根據線程獲取鎖的搶占機制,鎖可以分為公平鎖和非公平鎖。
公平鎖:表示線程獲取鎖的順序是按照線程請求鎖的時間早晚來決定的,也就是最早請求鎖的線程將最早獲取到鎖。
非公平鎖:非公平鎖則在運行時闖入,不遵循先到先執行的規則。
ReentrantLock:ReentrantLock 提供了公平和非公平鎖的實現。
ReentrantLock 實例:
//公平鎖
ReentrantLock pairLock = new ReentrantLock(true);
//非公平鎖
ReentrantLock pairLock1 = new ReentrantLock(false);
//如果構造函數不傳遞參數,則默認是非公平鎖。
ReentrantLock pairLock2 = new ReentrantLock();
場景介紹:通過模擬一個場景假設,來了解公平鎖與非公平鎖。
- 假設線程 A 已經持有了鎖,這時候線程 B 請求該鎖將會被掛起;
- 當線程 A 釋放鎖后,假如當前有線程 C 也需要獲取該鎖,如果采用非公平鎖方式,則根據線程調度策略,線程 B 和線程 C 兩者之一可能獲取鎖,這時候不需要任何其他干涉;
- 而如果使用公平鎖則需要把 C 掛起,讓 B 獲取當前鎖,因為 B 先到所以先執行。
Tips:在沒有公平性需求的前提下盡量使用非公平鎖,因為公平鎖會帶來性能開銷。
6. lockInterruptibly 方法
lockInterruptibly () 方法:能夠中斷等待獲取鎖的線程。當兩個線程同時通過 lock.lockInterruptibly () 獲取某個鎖時,假若此時線程 A 獲取到了鎖,而線程 B 只有等待,那么對線程 B 調用 threadB.interrupt () 方法能夠中斷線程 B 的等待過程。
場景設計:
- 創建兩個線程,創建方式可自選實現;
- 第一個線程先調用 start 方法,沉睡 20 毫秒后調用第二個線程的 start 方法,確保第一個線程先獲取鎖,第二個線程進入等待;
- 最后調用第二個線程的 interrupt 方法,終止線程;
- run 方法的邏輯為打印 0,1,2,3,4,每打印一個數字前,先沉睡 1000 毫秒;
- 觀察結果,看是否第二個線程被終止。
實例:
public class DemoTest{
private Lock lock = new ReentrantLock();
public void doBussiness() {
String name = Thread.currentThread().getName();
try {
System.out.println(name + " 開始獲取鎖");
lock.lockInterruptibly(); //調用lockInterruptibly方法,表示可中斷等待
System.out.println(name + " 得到鎖,開工干活");
for (int i=0; i<5; i++) {
Thread.sleep(1000);
System.out.println(name + " : " + i);
}
} catch (InterruptedException e) {
System.out.println(name + " 被中斷");
} finally {
try {
lock.unlock();
System.out.println(name + " 釋放鎖");
} catch (Exception e) {
System.out.println(name + " : 沒有得到鎖的線程運行結束");
}
}
}
public static void main(String[] args) throws InterruptedException {
final DemoTest lockTest = new DemoTest();
Thread t0 = new Thread(new Runnable() {
public void run() {
lockTest.doBussiness();
}});
Thread t1 = new Thread(new Runnable() {
public void run() {
lockTest.doBussiness();
}});
t0. start();
Thread.sleep(20);
t1. start();
t1.interrupt();
}
}
結果驗證:可以看到,thread -1 被中斷了。
Thread-0 開始獲取鎖
Thread-0 得到鎖,開工干活
Thread-1 開始獲取鎖
Thread-1 被中斷
Thread-1 : 沒有得到鎖的線程運行結束
Thread-0 : 0
Thread-0 : 1
Thread-0 : 2
Thread-0 : 3
Thread-0 : 4
Thread-0 釋放鎖
7. ReentrantLock 其他方法介紹
對 ReentrantLock 來說,方法很多樣,如下介紹 ReentrantLock 其他的方法,有興趣的同學可以自行的嘗試使用。
- getHoldCount():當前線程調用 lock () 方法的次數;
- getQueueLength():當前正在等待獲取 Lock 鎖的線程的估計數;
- getWaitQueueLength(Condition condition):當前正在等待狀態的線程的估計數,需要傳入 Condition 對象;
- hasWaiters(Condition condition):查詢是否有線程正在等待與 Lock 鎖有關的 Condition 條件;
- hasQueuedThread(Thread thread):查詢指定的線程是否正在等待獲取 Lock 鎖;
- hasQueuedThreads():查詢是否有線程正在等待獲取此鎖定;
- isFair():判斷當前 Lock 鎖是不是公平鎖;
- isHeldByCurrentThread():查詢當前線程是否保持此鎖定;
- isLocked():查詢此鎖定是否由任意線程保持。
8. 小結
本節內容對 ReentrantLock 進行了比較詳細的講解,通篇內容皆為重點內容,需要同學們進行細致的掌握。核心內容即為 ReentrantLock 的使用,可以根據小節中的實例進行自行的編碼和試驗,更深刻的理解 ReentrantLock 的使用。