歡迎來到《併發王者課》,本文是該系列文章中的第15篇。
在上篇文章中,我們介紹了Java中鎖的基礎Lock介面。在本文中,我們將介紹Java中鎖的另外一個重要的基本型介面,即ReadWriteLock介面。
在探索Java中的併發時,ReadWriteLock無疑是重要的,然而理解它卻並不容易。如果你此前曾經檢索資料,應該會發現大部分的文章對它的描述都比較晦澀難懂,或連篇累牘的原始碼陳列,或隔靴搔癢的三言兩語,既說不到重點,也說不清來龍去脈。
所以,在本文中我們會將介紹的重點放在對思路的理解上,而不是對原始碼的解讀上。對於原始碼以及其背後的知識,我們將在後面的更高階的系列中進行講解。
一、理解ReadWriteLock存在的價值
理解ReadWriteLock,首頁要理解它存在的意義是什麼。換言之,它要解決什麼問題。為此,我們不妨從下圖著手一探究竟。
不知你看明白了沒有,這幅圖所表達的有三層含義:
- 大量執行緒在競爭同一份資源;
- 這些執行緒中有的是讀請求,有的是寫請求;
- 在多個執行緒的請求中,讀請求明顯高於寫請求。
這樣的場景是否似曾相識?沒錯,它就是典型的快取應用場景。
眾所周知,快取的存在是為了提高應用的讀寫效能。一方面,我們需要通過快取攔截大量的讀資料的請求。另一方面,我們也需要不定期地更新快取。但總體而言,更新快取的次數遠遠小於讀快取的次數。
在這個過程中,關鍵問題在於,為了保持資料一致性,我們在讀寫快取的時候,不能讓讀請求拿到髒資料,這就需要用到鎖。然而,更關鍵的問題在於,雖然讀寫之間需要互斥,但讀與讀之間不可以互斥。
總結來說,這個問題主要有下面這幾個要點:
- 資料允許多個執行緒同時讀取,但只允許一個執行緒進行寫入;
- 在讀取資料的時候,不可以存在寫操作或者寫請求;
- 在寫資料的時候,不可以存在讀請求。
如果你對此仍然有些迷茫,那麼下面這張圖建議你收藏,這張圖正是ReadWriteLock對問題的概述和它的解決方案,也是詮釋ReadWriteLock最好的一幅圖。
在你沒有理解ReadWriteLock之前,你會覺得它十分晦澀且原始碼枯燥。然而,一旦你理解它要解決的問題,以及它所提供的方案後,你會發現它的設計竟然如此巧妙。它竟然設計了兩種截然不同的鎖,其中一把正如我們此前認知的那樣是執行緒互斥的,而另一把鎖竟然可以為多個執行緒所共享!兩把鎖的完美配合,解決了併發讀寫的場景問題。
在恍然大悟後,所謂原始碼不過是佇列與共享,它們是ReadWriteLock的一種實現方式,而不是阻擋你理解的絆腳石。
二、自主實現ReadWriteLock
在理解了ReadWriteLock背後的問題和它的解決思路之後,我們就可以完全拋開JDK中的原始碼自己實現一把讀寫鎖。
public class ReadWriteLock{
private int readers = 0;
private int writers = 0;
private int writeRequests = 0;
public synchronized void lockRead() throws InterruptedException{
while(writers > 0 || writeRequests > 0){
wait();
}
readers++;
}
public synchronized void unlockRead(){
readers--;
notifyAll();
}
public synchronized void lockWrite() throws InterruptedException{
writeRequests++;
while(readers > 0 || writers > 0){
wait();
}
writeRequests--;
writers++;
}
public synchronized void unlockWrite() throws InterruptedException{
writers--;
notifyAll();
}
}
在讀鎖lockRead()
中,是不允許有寫請求或寫操作的。如果有,那麼讀請求將進入等待。
而在lockWrite()
中,同時不允許讀請求和其他寫操作的存在,此時只允許有一個寫請求。
以上就是讀寫鎖簡單的自主實現方式。當然,它是不完善的,只是基本的示例。它沒有考慮到基本的執行緒重入問題,真實情況也比它複雜很多,但你理解它的意思就好。
三、Java中的ReadWriteLock是如何實現的
最後,我們再來看JDK中的ReadWriteLock實現的一些基本思路。ReadWriteLock和我們上篇所說的Lock介面以及其他類的基本關係如下圖所示:
可以看到,JDK中的讀寫鎖的實現是在ReentrantReadWriteLock這個類中。ReentrantReadWriteLock包含了兩個內部類:ReadLock和WriteLock,而這兩個類又實現了Lock介面。
讀寫鎖的升級與降級
讀寫鎖的升級與降級是ReentrantReadWriteLock中的一個重要知識點,也是高頻的面試題。
從讀鎖到寫鎖,稱之為鎖的升級,反之為鎖的降級。理解讀寫鎖的升級和降級,最直觀的方式是寫程式碼驗證。
程式碼片段1,先獲取讀鎖,再獲取寫鎖。
public class ReadWriteLockDemo {
public static void main(String[] args) {
ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
readWriteLock.readLock().lock();
System.out.println("已經獲取讀鎖...");
readWriteLock.writeLock().lock();
System.out.println("已經獲取寫鎖...");
}
}
輸出結果如下:
已經獲取讀鎖...
程式碼片段2,先獲取寫鎖,再獲取讀鎖:
public class ReadWriteLockDemo {
public static void main(String[] args) {
ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
readWriteLock.writeLock().lock();
System.out.println("已經獲取寫鎖...");
readWriteLock.readLock().lock();
System.out.println("已經獲取讀鎖...");
}
}
輸出結果如下:
已經獲取寫鎖...
已經獲取讀鎖...
Process finished with exit code 0
這樣一來,結果已經十分明瞭。ReentrantReadWriteLock支援鎖的降級,但不支援鎖的升級。
讀寫鎖中的公平性
在前面的文章中,我們講過執行緒飢餓的由來和後果,所以良好的併發工具類在設計時都會考慮到公平性,ReentrantReadWriteLock也是如此。
在ReentrantReadWriteLock中,同時提供了公平和非公平兩種模式,且預設為非公平模式。從下面摘取的原始碼片段中,可以清晰地看到。
public ReentrantReadWriteLock() {
this(false);
}
/**
/**
* Creates a new {@code ReentrantReadWriteLock} with
* default (nonfair) ordering properties.
*/
public ReentrantReadWriteLock() {
this(false);
}
/**
* Creates a new {@code ReentrantReadWriteLock} with
* the given fairness policy.
*
* @param fair {@code true} if this lock should use a fair ordering policy
*/
public ReentrantReadWriteLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
readerLock = new ReadLock(this);
writerLock = new WriteLock(this);
}
小結
以上就是關於讀寫鎖的全部內容。在本文中,我們從快取問題出發,接著從ReadWriteLock中尋找答案,以便能從更輕鬆的角度理解ReadWriteLock的來龍去脈。
理解ReadWriteLock的關鍵不在於對原始碼的剖析,而在於對其思路的理解。
另外,我們簡單地介紹了ReentrantReadWriteLock中的一些關鍵知識點,但諸如其背後的AQS等並沒有展開陳述。對此也不必著急,我們會在後面有詳細的分析介紹。
正文到此結束,恭喜你又上了一顆星✨
夫子的試煉
- 嘗試在示例程式碼中增加對讀寫執行緒的重入支援。
延伸閱讀與參考資料
關於作者
關注公眾號【庸人技術笑談】,獲取及時文章更新。記錄平凡人的技術故事,分享有品質(儘量)的技術文章,偶爾也聊聊生活和理想。不販賣焦慮,不做標題黨。
如果本文對你有幫助,歡迎點贊、關注、監督,我們一起從青銅到王者。