歡迎來到《併發王者課》,本文是該系列文章中的第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介面,自定義實現一把鎖。
延伸閱讀與參考資料
關於作者
關注公眾號【庸人技術笑談】,獲取及時文章更新。記錄平凡人的技術故事,分享有品質(儘量)的技術文章,偶爾也聊聊生活和理想。不販賣焦慮,不做標題黨。
如果本文對你有幫助,歡迎點贊、關注、監督,我們一起從青銅到王者。