在進行Java多線程編程的過程中,始終繞不開一個問題:線程安全。一般來說,我們可以通過對一些資源加鎖來實現,大多都是通過 synchronized
關鍵字實現。
在做性能測試時,如果TPS或者QPS要求沒有特別高, synchronized
一招鮮基本也能滿足大部分的需求了。
對於一招鮮無法很好解決的問題,就需要我們繼續探索 java.util.concurrent
包的其他內容。今天就分享一下 java.util.concurrent.locks.Lock
接口的實現類 java.util.concurrent.locks.ReentrantLock
的基本使用方法。
類功能概覽
java.util.concurrent.locks.Lock
接口支持三種方法的鎖獲取:阻塞鎖、可中斷鎖和超時鎖。
下面來分享這幾種鎖的常用的使用場景和案例。
阻塞鎖
方法是:java.util.concurrent.locks.ReentrantLock#lock
,沒有參數。該方法會嘗試獲取鎖。當無法獲取鎖時,當前線程會處於休眠狀態,直到獲取鎖成功。
演示Demo如下:
private static final Logger log = LogManager.getLogger(LockTest.class);
public static void main(String[] args) throws InterruptedException {
ReentrantLock lock = new ReentrantLock();
Thread lockTestThread = new Thread(() -> {
lock.lock();
log.info("獲取到鎖了!");
lock.unlock();
});
lock.lock();
lockTestThread.start();
log.info("即將馬上釋放鎖!");
Thread.sleep(1000);
lock.unlock();
lockTestThread.join();
}
控制檯打印:
19:43:29 046 main 即將馬上釋放鎖!
19:43:30 050 Thread-2 獲取到鎖了!
19:43:30 uptime:1 s
由於異步線程獲取鎖的方法晚於 main
線程,所以會在獲取鎖的地方阻塞,直至 main
線程將鎖釋放。可以看到,兩條打印日誌相差約1s。
可中斷鎖
可中斷鎖API是:java.util.concurrent.locks.ReentrantLock#lockInterruptibly
。該方式會嘗試獲取鎖,並且是阻塞的,但當未獲取到鎖時,如果當前線程被設置了中斷狀態,則會拋出 java.lang.InterruptedException
異常。
演示Demo如下:
private static final Logger log = LogManager.getLogger(LockTest.class);
public static void main(String[] args) throws InterruptedException {
ReentrantLock lock = new ReentrantLock();
Thread lockTestThread = new Thread(() -> {
try {
lock.lockInterruptibly();
log.info("獲取到鎖了!");
lock.unlock();
} catch (InterruptedException e) {
log.warn("獲取鎖失敗!", e);
}
});
lock.lock();
lockTestThread.start();
lockTestThread.interrupt();
lock.unlock();
lockTestThread.join();
}
控制檯打印:
19:58:21 250 Thread-2 獲取鎖失敗!
java.lang.InterruptedException: null
at java.util.concurrent.locks.AbstractQueuedSynchronizer.acquireInterruptibly(AbstractQueuedSynchronizer.java:1220) ~[?:1.8.0_281]
at java.util.concurrent.locks.ReentrantLock.lockInterruptibly(ReentrantLock.java:335) ~[?:1.8.0_281]
at com.funtest.temp.LockTest.lambda$main$0(LockTest.java:18) ~[classes/:?]
at java.lang.Thread.run(Thread.java:748) [?:1.8.0_281]
超時鎖
超時鎖的API有兩個:java.util.concurrent.locks.ReentrantLock#tryLock()
和 java.util.concurrent.locks.ReentrantLock#tryLock(long, java.util.concurrent.TimeUnit)
,返回1個Boolean值,表示獲取鎖是否成功。第二個API參數設置超時時間。這兩個API前者可以簡單理解爲後者時間設置爲0,獲取一下試試,成不成都返回結果。
演示Demo如下:
private static final Logger log = LogManager.getLogger(LockTest.class);
public static void main(String[] args) throws InterruptedException {
ReentrantLock lock = new ReentrantLock();
Thread lockTestThread = new Thread(() -> {
boolean b = lock.tryLock();
log.info("第一次獲取鎖的結果:{}", b);
try {
boolean b1 = lock.tryLock(3, TimeUnit.SECONDS);
log.info("第二次獲取鎖的結果:{}", b1);
} catch (InterruptedException e) {
log.warn("第二次獲取鎖的時候被中斷了");
}
});
lock.lock();
lockTestThread.start();
Thread.sleep(1000);
lock.unlock();
lockTestThread.join();
}
控制檯打印:
20:05:13 559 Thread-2 第一次獲取鎖的結果:false
20:05:14 563 Thread-2 第二次獲取鎖的結果:true
20:05:14 uptime:2 s
可以看到再等待了 1s 之後,第二次獲取鎖成功了。爲了簡化代碼,我並沒有寫判斷獲取鎖狀態的代碼。
最佳實踐
對於 java.util.concurrent.locks.ReentrantLock
,常用最佳實踐只有一個,非常容易掌握。那就是使用 try-catch-finally
語法實現,演示Demo如下:
boolean status = false;
try {
status = lock.tryLock(3, TimeUnit.SECONDS);
} catch (Exception e) {
// 異常處理
} finally {
if (status) lock.unlock();
}
- 儘量使用超時鎖
- 儘可能少佔用鎖
- 儘量低頻使用
可重入
java.util.concurrent.locks.ReentrantLock
直譯就是可重入鎖,意思是當一個線程獲取到鎖之後,還可以再獲取一次,當然釋放也需要兩次。在內部有專門用來計數的功能,當然也是線程安全的。
在性能測試實踐中,很少能遇到使用 可重入
的特性的場景。所以這裏建議不要過度使用 java.util.concurrent.locks.ReentrantLock
,複雜場景可以有更加簡單可靠的解決方案。
公平鎖與非公平鎖
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值,含義既是是否使用公平鎖。無參的構造方法默認使用的非公平鎖。公平鎖和非公平鎖的主要區別是獲取鎖的方式不同。公平鎖的獲取是公平的,線程依次排隊獲取鎖。誰等待的時間最長,就由誰獲得鎖。非公平鎖獲取是隨機的,誰先請求誰先獲得鎖,不一定按照請求鎖的順序來。
具體區別如下:
- 獲取鎖的方式不同
- 公平鎖:線程依次排隊獲取鎖,效率較低
- 非公平鎖:隨機獲取鎖,效率較高
- 性能不同
- 公平鎖:一次性喚醒隊列中等待時間最久的線程,Context Switching次數高,性能較低
- 非公平鎖:隨機喚醒線程,Context Switching次數低,性能較高
- 鎖等待時間
- 公平鎖:等待時間長,但訪問順序按隊列順序
- 非公平鎖:等待時間短,但訪問順序隨機
- 影響因素
- 公平鎖:隻影響當前等待的線程,不影響新來線程
- 非公平鎖:可能會無限次讓新來線程搶佔鎖,導致老線程永遠獲取不到鎖
- 線程飢餓
- 公平鎖:舊線程有獲取鎖的機會,相對更公平
- 非公平鎖:可能導致線程飢餓問題
所以綜上,非公平鎖性能更高,但公平鎖更公平。由於性能測試中通常對性能是有要求的,若非強需求,建議儘量使用非公平鎖。