併發王者課-鉑金2:豁然開朗-“晦澀難懂”的ReadWriteLock竟如此妙不可言

秦二爺發表於2021-06-18

歡迎來到《併發王者課》,本文是該系列文章中的第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等並沒有展開陳述。對此也不必著急,我們會在後面有詳細的分析介紹。

正文到此結束,恭喜你又上了一顆星✨

夫子的試煉

  • 嘗試在示例程式碼中增加對讀寫執行緒的重入支援。

延伸閱讀與參考資料

關於作者

關注公眾號【庸人技術笑談】,獲取及時文章更新。記錄平凡人的技術故事,分享有品質(儘量)的技術文章,偶爾也聊聊生活和理想。不販賣焦慮,不做標題黨。

如果本文對你有幫助,歡迎點贊關注監督,我們一起從青銅到王者

相關文章