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