歡迎來到《並發王者課》,本文是該系列文章中的第14篇。
在黃金系列中,我們介紹了並發中一些問題,比如死鎖、活鎖、線程飢餓等問題。在並發編程中,這些問題無疑都是需要解決的。所以,在鉑金系列文章中,我們會從並發中的問題出發,探索Java所提供的鎖的能力以及它們是如何解決這些問題的。
作為鉑金系列文章的第一篇,我們將從Lock接口開始介紹,因為它是Java中鎖的基礎,也是並發能力的基礎。
一、理解Java中鎖的基礎:Lock接口
在青銅系列文章中,我們介紹了通過synchronized
關鍵字實現對方法和代碼塊加鎖的用法。然而,雖然synchronized
非常好用、易用,但是它的靈活度卻十分有限,不能靈活地控制加鎖和釋放鎖的時機。所以,為了更靈活地使用鎖,並滿足更多的場景需要,就需要我們能夠自主地定義鎖。於是,就有了Lock接口。
理解Lock最直觀的方式,莫過於直接在JDK所提供的並發工具類中找到它,如下圖所示:
可以看到,Lock接口提供了一些能力API,並有一些具體的實現,如ReentrantLock、ReentrantReadWriteLock等。
1. Lock的五個核心能力API
void lock()
:獲取鎖。如果當前鎖不可用,則會被阻塞直至鎖釋放;void lockInterruptibly()
:獲取鎖並允許被中斷。這個方法和lock()
類似,不同的是,它允許被中斷並拋出中斷異常。boolean tryLock()
:嘗試獲取鎖。會立即返回結果,而不會被阻塞。boolean tryLock(long timeout, TimeUnit timeUnit)
:嘗試獲取鎖並等待一段時間。這個方法和tryLock()
,但是它會根據參數等待–會,如果在規定的時間內未能獲取到鎖就會放棄;void unlock()
:釋放鎖。
2. Lock的常見實現
在Java並發工具類中,Lock接口有一些實現,比如:
- ReentrantLock:可重入鎖;
- ReentrantReadWriteLock:可重入讀寫鎖;
除了列舉的兩個實現外,還有一些其他實現類。對於這些實現,暫且不必詳細了解,后面會詳細介紹。在目前階段,你需要理解的是Lock是它們的基礎。
二、自定義Lock
接下來,我們基於前面的示例代碼,看看如何將synchronized
版本的鎖用Lock來實現。
public static class WildMonster {
private boolean isWildMonsterBeenKilled;
public synchronized void killWildMonster() {
String playerName = Thread.currentThread().getName();
if (isWildMonsterBeenKilled) {
System.out.println(playerName + "未斬殺野怪失敗...");
return;
}
isWildMonsterBeenKilled = true;
System.out.println(playerName + "斬獲野怪!");
}
}
1. 實現一把簡單的鎖
創建類WildMonsterLock並實現Lock接口,WildMonsterLock將是取代synchronized
的關鍵:
// 自定義鎖
public class WildMonsterLock implements Lock {
private boolean isLocked = false;
// 實現lock方法
public void lock() {
synchronized (this) {
while (isLocked) {
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
isLocked = true;
}
}
// 實現unlock方法
public void unlock() {
synchronized (this) {
isLocked = false;
this.notify();
}
}
}
在實現Lock接口時,你需要實現它上述的所有方法。不過,為了簡化代碼方便展示,我們移除了WildMonsterLock類中的tryLock
等方法。
對於wait
和notify
方法的時候,如果你不熟悉的話,可以查看青銅系列的文章。這里需要提醒的是,notify
在使用時務必要和wait
是同一個監視器。
基於剛才定義的WildMonsterLock,創建WildMonster類,並在方法killWildMonster中使用WildMonsterLock對象,從而取代synchronized.
// 使用剛才自定義的鎖
public class WildMonster {
private boolean isWildMonsterBeenKilled;
// 創建鎖對象
private Lock lock = new WildMonsterLock();
public void killWildMonster() {
// 獲取鎖
lock.lock();
try {
String playerName = Thread.currentThread().getName();
if (isWildMonsterBeenKilled) {
System.out.println(playerName + "未斬殺野怪失敗...");
return;
}
isWildMonsterBeenKilled = true;
System.out.println(playerName + "斬獲野怪!");
} finally {
// 執行結束后,無論如何不要忘記釋放鎖
lock.unlock();
}
}
}
輸出結果如下:
哪吒斬獲野怪!
典韋未斬殺野怪失敗...
蘭陵王未斬殺野怪失敗...
鎧未斬殺野怪失敗...
Process finished with exit code 0
從結果中可以看到:只有哪吒一人斬獲了野怪,其他幾個英雄均以失敗告終,結果符合預期。這說明,WildMonsterLock達到了和synchronized
一致的效果。
不過,這里有細節需要注意。在使用synchronized
時我們無需關心鎖的釋放,JVM會幫助我們自動完成。然而,在使用自定義的鎖時,一定要使用try...finally
來確保鎖最終一定會被釋放,否則將造成后續線程被阻塞的嚴重后果。
2. 實現可重入的鎖
在synchronized
中,鎖是可以重入的。所謂鎖的可重入,指的是鎖可以被線程重復或遞歸調用。比如,加鎖對象中存在多個加鎖方法時,當線程在獲取到鎖進入其中任一方法后,線程應該可以同時進入其他的加鎖方法,而不會出現被阻塞的情況。當然,前提條件是這個加鎖的方法用的是同一個對象的鎖(監視器)。
在下面這段代碼中,方法A和B都是同步方法,並且A中調用B. 那么,線程在調用A時已經獲得了當前對象的鎖,那么線程在A中調用B時可以直接調用,這就是鎖的可重入性。
public class WildMonster {
public synchronized void A() {
B();
}
public synchronized void B() {
doSomething...
}
}
所以,為了讓我們自定義的WildMonsterLock也支持可重入,我們需要對代碼進行一點改動。
public class WildMonsterLock implements Lock {
private boolean isLocked = false;
// 重點:增加字段保存當前獲得鎖的線程
private Thread lockedBy = null;
// 重點:增加字段記錄上鎖次數
private int lockedCount = 0;
public void lock() {
synchronized (this) {
Thread callingThread = Thread.currentThread();
// 重點:判斷是否為當前線程
while (isLocked && lockedBy != callingThread) {
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
isLocked = true;
lockedBy = callingThread;
lockedCount++;
}
}
public void unlock() {
synchronized (this) {
// 重點:判斷是否為當前線程
if (Thread.currentThread() == this.lockedBy) {
lockedCount--;
if (lockedCount == 0) {
isLocked = false;
this.notify();
}
}
}
}
}
在新的WildMonsterLock中,我們增加了this.lockedBy
和lockedCount
字段,並在加鎖和解鎖時增加對線程的判斷。在加鎖時,如果當前線程已經獲得鎖,那么將不必進入等待。而在解鎖時,只有當前線程能解鎖。
lockedCount
字段則是為了保證解鎖的次數和加鎖的次數是匹配的,比如加鎖了3次,那么相應的也要3次解鎖。
3. 關注鎖的公平性
在黃金系列文章中,我們提到了線程在競爭中可能被餓死,因為競爭並不是公平的。所以,我們在自定義鎖的時候,也應當考慮鎖的公平性。
三、小結
以上就是關於Lock的全部內容。在本文中,我們介紹了Lock是Java中各類鎖的基礎。它是一個接口,提供了一些能力API,並有着完整的實現。並且,我們也可以根據需要自定義實現鎖的邏輯。所以,在學習Java中各種鎖的時候,最好先從Lock接口開始。同時,在替代synchronized的過程中,我們也能感受到Lock有一些synchronized所不具備的優勢:
-
synchronized用於方法體或代碼塊,而Lock可以靈活使用,甚至可以跨越方法;
-
synchronized沒有公平性,任何線程都可以獲取並長期持有,從而可能餓死其他線程。而基於Lock接口,我們可以實現公平鎖,從而避免一些線程活躍性問題;
-
synchronized被阻塞時只有等待,而Lock則提供了
tryLock
方法,可以快速試錯,並可以設定時間限制,使用時更加靈活; -
synchronized不可以被中斷,而Lock提供了
lockInterruptibly
方法,可以實現中斷。
另外,在自定義鎖的時候,要考慮鎖的公平性。而在使用鎖的時候,則需要考慮鎖的安全釋放。
夫子的試煉
- 基於Lock接口,自定義實現一把鎖。
延伸閱讀與參考資料
關於作者
關注公眾號【庸人技術笑談】,獲取及時文章更新。記錄平凡人的技術故事,分享有品質(盡量)的技術文章,偶爾也聊聊生活和理想。不販賣焦慮,不做標題黨。
如果本文對你有幫助,歡迎點贊、關注、監督,我們一起從青銅到王者。