2.3 lock 鎖
如果你曾經遭遇過執行緒不安全的問題,一定不會對 “鎖” 這個概念不陌生。實際上絕大多數執行緒安全的先解決方案都離不開 “鎖”。
JDK 裡面就有一個介面java.util.concurrent.locks.Lock
,顧名思義,就是併發包中 “鎖”,大量的執行緒安全問題解決方案均是依賴這個介面的實現類。就跟 synchronized 關鍵字一樣,在效能測試實戰中只要掌握基本的功能和最佳實戰即可,這裡再重複一下上一節的建議:如需使用 Lock 實現的功能過於複雜,建議拋開 Lock,尋找更加簡單、可靠,已驗證的解決方案。
在效能測試中最常用的java.util.concurrent.locks.Lock
實現類就是可重入鎖:java.util.concurrent.locks.ReentrantLock
。相比synchronized
,ReentrantLock
擁有以下主要優點:
可重入性。ReentrantLock
允許已經獲取鎖的執行緒再次獲取鎖,相比 d 更加安全,避免發生死鎖的情況。
更加靈活。d 提供多個 API 完成鎖的獲取和釋放,讓使用者擁有更多選擇。
可中斷性。ReentrantLock
功能中,獲取鎖的執行緒可以被主動中斷,相比synchronized
無限等待,更加適合處理鎖的超時場景。
更高的效能。除了提供多種獲取鎖的 API 以外,ReentrantLock
還提供兩種鎖型別:公平鎖和非公平鎖,幫助程式提升在加鎖場景的效能。
ReentrantLock
提供了 3 中獲取鎖的 API,分別是:阻塞鎖、可中斷鎖和超時鎖。下面分別用程式碼演示如何使用。
2.3.1 阻塞鎖
獲取阻塞鎖的方法是:java.util.concurrent.locks.ReentrantLock#lock
,沒有引數。該方法會嘗試獲取鎖。當無法獲取鎖時,當前執行緒會處於休眠狀態,直到獲取鎖成功。
演示程式碼如下:
package org.funtester.performance.books.chapter02.section3;
import java.util.concurrent.locks.ReentrantLock;
/**
* 阻塞鎖示例
*/
public class BlockingLockDemo {
public static void main(String[] args) throws InterruptedException {
ReentrantLock lock = new ReentrantLock();// 建立一個可重入鎖
Thread lockTestThread = new Thread(() -> {// 建立一個執行緒
System.out.println(System.currentTimeMillis() + " 非同步執行緒啟動! " + Thread.currentThread().getName());// 列印日誌
lock.lock();// 獲取鎖
System.out.println(System.currentTimeMillis() + " 獲取到鎖了! " + Thread.currentThread().getName());// 列印日誌
lock.unlock();// 釋放鎖
});
lock.lock();// 獲取鎖
lockTestThread.start();// 啟動非同步執行緒
Thread.sleep(100);// 睡眠100毫秒
System.out.println(System.currentTimeMillis() + " 即將釋放鎖! " + Thread.currentThread().getName());// 列印日誌
lock.unlock();// 釋放鎖
}
}
這個例子中,首先建立了一個非同步執行緒,執行程式碼邏輯為:獲取鎖,列印日誌,釋放鎖。然後在 main 執行緒中,先獲取鎖,再啟動非同步執行緒。然後main
執行緒休眠 100 毫秒,再釋放鎖。控制檯輸出內容如下:
1698477535368 非同步執行緒啟動! Thread-0
1698477535471 即將釋放鎖! main
1698477535471 獲取到鎖了! Thread-0
可以看到,非同步執行緒在啟動之後,等待了 100 毫秒才獲取到鎖,並列印日誌,且這個操作也是在 main 執行緒釋放鎖之後進行的。原因是因為 main 執行緒先於非同步執行緒獲取到鎖了,所以在 main 執行緒釋放鎖之前,非同步執行緒只能無所事事,乾等著。
阻塞鎖和synchronized
解決執行緒安全思路和使用方法上比較相似,而且在效能測試工作中使用場景大多重合。阻塞鎖在一定程度上可以替代synchronized
,特別是在編寫流程式的程式碼中。
2.3.2 可中斷鎖
可中斷鎖的獲取方法是:java.util.concurrent.locks.ReentrantLock#lockInterruptibly
,沒有引數。該方式會嘗試獲取鎖,並且是阻塞的,但當未獲取到鎖時,如果當前執行緒被設定了中斷狀態,則會丟擲 java.lang.InterruptedException 異常。
下面是演示程式碼:
package org.funtester.performance.books.chapter02.section3;
import java.util.concurrent.locks.ReentrantLock;
/**
* 可中斷鎖示例
*/
public class InterruptiblyLockDemo {
public static void main(String[] args) throws InterruptedException {
ReentrantLock lock = new ReentrantLock();// 建立一個可重入鎖
Thread lockTestThread = new Thread(() -> {// 建立一個執行緒
try {
lock.lockInterruptibly();// 獲取鎖
System.out.println(System.currentTimeMillis() + " 獲取到鎖了! " + Thread.currentThread().getName());// 列印日誌
lock.unlock();// 釋放鎖
} catch (InterruptedException e) {
System.out.println(System.currentTimeMillis() + " 執行緒被中斷了! " + Thread.currentThread().getName());// 列印日誌
}
});
lock.lock();// 獲取鎖
lockTestThread.start();// 啟動非同步執行緒
lockTestThread.interrupt();// 中斷非同步執行緒
lock.unlock();// 釋放鎖
}
}
在這個例子中,首先建立了一個非同步執行緒,執行程式碼邏輯為獲取鎖(可中斷),列印日誌,釋放鎖。然後讓 main 執行緒先獲取鎖,然後啟動非同步執行緒,再中斷非同步執行緒。下面來就控制檯輸出:
1698478061924 執行緒被中斷了! Thread-0
這裡看到只有一行輸出,即非同步執行緒再獲取鎖時被中斷了,丟擲的異常被捕獲。
可中斷鎖繼承了阻塞鎖的有點,提供了將執行緒從等待中解脫的方案,在使用上更加廣泛。可中斷鎖可以進行執行緒間超時控制、防止無限等待,可以非常優雅地關閉被阻塞的執行緒,釋放資源。可中斷鎖適合多執行緒協作的場景,要求使用者對多執行緒瞭解也更高。
2.3.3 超時鎖
超時鎖的獲取方法有兩個:java.util.concurrent.locks.ReentrantLock#tryLock()
和 java.util.concurrent.locks.ReentrantLock#tryLock(long, java.util.concurrent.TimeUnit)
,返回Boolean
值,表示獲取鎖是否成功。第二個 API 引數設定超時時間。這兩個 API 前者可以簡單理解為後者時間設定為 0,含義是嘗試獲取一次,返回結果。
演示程式碼如下:
package org.funtester.performance.books.chapter02.section3;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;
/**
* 超時鎖示例
*/
public class TimeoutLockDemo {
public static void main(String[] args) throws InterruptedException {
ReentrantLock lock = new ReentrantLock();// 建立一個可重入鎖
Thread lockTestThread = new Thread(() -> {// 建立一個執行緒
boolean b = lock.tryLock();// 第一次嘗試獲取鎖
System.out.println(System.currentTimeMillis() + " 第一次獲取鎖的結果:" + b + " " + Thread.currentThread().getName());// 列印日誌
try {
boolean b1 = lock.tryLock(3, TimeUnit.SECONDS);
System.out.println(System.currentTimeMillis() + " 第二次獲取鎖的結果:" + b1 + " " + Thread.currentThread().getName());
} catch (InterruptedException e) {
System.out.println(System.currentTimeMillis() + " 第二次獲取鎖中斷了 " + Thread.currentThread().getName());
}
});
lock.lock();// 獲取鎖
lockTestThread.start();// 啟動非同步執行緒
Thread.sleep(100);// 睡眠100毫秒
lock.unlock();// 釋放鎖
}
}
在這個例子中,依舊先建立一個非同步執行緒,執行的邏輯為:首先嚐試獲取一次並且列印結果,然後第二次嘗試獲取,設定超時時間 3 秒,並列印結果。main 執行緒依舊先獲取鎖,然後啟動非同步執行緒,休眠 100 ms 然後釋放鎖。例子中,為了簡化程式碼,筆者並沒有編寫依據獲取鎖的結果釋放鎖的程式碼。控制檯輸出內容如下:
1698479430990 第一次獲取鎖的結果:false Thread-0
1698479431090 第二次獲取鎖的結果:true Thread-0
可以看到第一次獲取鎖失敗了,原因是該鎖正在被 main 執行緒持有。第二獲取鎖成功了,因為 main 執行緒持有鎖 100 毫秒之後便釋放鎖。在非同步執行緒第二次獲取鎖的 3 秒超時時間內,它成功了,所以獲取到了鎖。
在三種鎖的方法中,超時鎖在效能測試中使用最廣泛。它提供了一種簡單、可靠的控制鎖等待時間的方式。相比可中斷鎖,超時鎖對新手更加容易上手,無須掌握執行緒間統信、排程的知識。
2.3.4 公平鎖和非公平鎖
java.util.concurrent.locks.ReentrantLock 有一個構造方法,如下:
/**
* Creates an instance of {@code ReentrantLock} with the
* given fairness policy. * * @param fair {@code true} if this lock should use a fair ordering policy
*/public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
方法引數中Boolean
值,含義即是否使用公平鎖。無參的構造方法預設使用的非公平鎖。公平鎖和非公平鎖的主要區別是獲取鎖的方式不同。公平鎖的獲取是公平的,執行緒依次排隊獲取鎖。誰等待的時間最長,就由誰獲得鎖。非公平鎖獲取是隨機的,誰先請求誰先獲得鎖,不一定按照請求鎖的順序來。ReentrantLock
預設的是非公平鎖,相比公平鎖擁有更高效能。
2.3.5 最佳實戰
在效能測試實戰中, java.util.concurrent.locks.ReentrantLock
而言 ,常用最佳實戰非常容易掌握。那就是使用 try-catch-finally
語法實現,演示案例如下:
boolean status = false;
try {
status = lock.tryLock(3, TimeUnit.SECONDS);
} catch (Exception e) {
// 異常處理
} finally {
if (status) lock.unlock();
}
在使用ReentrantLock
解決執行緒安全問題時,有幾點注意事項:
- 必須主動進行鎖管理。與
synchronized
不同,ReentrantLock
要求必需顯示獲取和釋放鎖,特別在釋放鎖時,最簡單的方法就是按照最佳實戰,將其放在 finally 中執行。 - 竭力避免死鎖。不要混合使用不同鎖;不要在一個功能中使用過多的鎖和
synchronized
關鍵字;避免多次獲取鎖;使用使用lockInterruptibly()
獲取鎖,如果在等待鎖的過程中執行緒被中斷,需要有處理程式碼進行後續處理。 - 儘量使用 ReentrantLock 預設的非公平鎖。
雖然java.util.concurrent.locks.ReentrantLock
叫可重入鎖,但是在效能測試實踐當中,不建議使用可重入功能。主要原因以下兩點:
- (1)增加鎖競爭,影響效能。使用不當會導致同一個執行緒頻繁獲取和釋放鎖,增加競爭,降低程式效能。
- (2)死鎖風險。如果同步程式碼中多次使用鎖,就需要嚴格釋放鎖流程,一旦發生異常而沒有捕獲處理,則會造成死鎖風險。
在筆者的效能測試生涯中,沒有必須使用可重入特性的場景,所以在效能測試實踐中,應當儘量避免使用該特性,防止異常情況發生。
書的名字:從 Java 開始做效能測試 。
如果本書內容對你有所幫助,希望各位不吝讚賞,讓我可以貼補家用。讚賞兩位數可以提前閱讀未公開章節。我也會嘗試製作本書的影片教程,包括必要的答疑。
FunTester 原創精華
【連載】從 Java 開始效能測試
- 混沌工程、故障測試、Web 前端
- 服務端功能測試
- 效能測試專題
- Java、Groovy、Go
- 白盒、工具、爬蟲、UI 自動化
- 理論、感悟、影片