簡單分析執行緒獲取ReentrantReadWriteLock 讀鎖的規則

kingsleylam發表於2019-07-24

 1. 問題

最近有同事問了我一個問題,在Java程式設計中,當有一條執行緒要獲取ReentrantReadWriteLock的讀鎖,此時已經有其他執行緒獲得了讀鎖,AQS佇列裡也有執行緒在等待寫鎖。由於讀鎖是共享鎖,當前執行緒是馬上獲得讀鎖,還是排隊?如果是馬上獲得讀鎖,那豈不是阻塞的等待寫鎖的執行緒有可能一直(或長時間)拿不到寫鎖(寫鎖飢餓)?

帶著這個問題,我開啟讀寫鎖的原始碼,來看一下JDK是怎麼實現的。(注:讀寫鎖指ReentrantReadWriteLock, 以下說到的讀鎖和寫鎖,都是指屬於同一個讀寫鎖的情況。讀鎖和共享鎖,寫鎖和獨佔鎖,在這裡是同樣的意思。如無特殊說明,提到的模式都是預設的非公平模式)

2. JUC萬物皆有AQS

2.1 讀鎖的實現。

先來看看讀鎖的實現。持有一個AQS,所以說,JUC萬物皆有AQS(大霧)。

順便提一下寫鎖,寫鎖也是類似的實現,而且傳入的是同一個讀寫鎖,那麼讀鎖和寫鎖,都擁有同一個AQS,這樣才能實現互相阻塞。

讀鎖是共享模式。

2.2 tryAcquireShared(int arg)的實現。

熟悉AQS的同學就知道,共享鎖的實現,AQS已經寫好了流程。但留下了一個鉤子,tryAcquireShared(int arg) 供各種場景實現。

那麼我們就來看看,讀寫鎖裡面,共享鎖(讀鎖)是怎麼實現的。

step1. 紅框一,如果當前已經有執行緒持有了獨佔鎖(即寫鎖),且不是當前執行緒持有,那麼無法重入,直接返回-1,獲取共享鎖失敗。

step2. 如果step1的情況被排除,那麼進行readerShouldBlock()的判斷。在讀寫鎖中,AQS有兩種實現,公平和非公平模式,預設是非公平模式。

也就是說,上面所說的sync變數的實際型別,可以是公平模式,也可以是非公平模式。

因此,readerShouldBlock()也有公平和非公平兩種不同的實現。

公平模式下,只要前面有阻塞排隊的節點,就返回true,表示不能搶佔。

非公平模式下,看看第一個等待的阻塞節點是不是獨佔式的,如果是,返回true,有可能不可以搶在人家前面(為什麼是有可能?要考慮可重入的場景,下面分析)。這是為了避免寫鎖飢餓。

所以,如果readerShouldBlock()返回false,並且讀鎖獲取的總次數不溢位,且CAS成功,說明獲取共享鎖成功,下面進入if塊,設定一些變數,並將當前執行緒持有的該讀鎖的次數遞增加1,返回成功標誌。

看到這裡,也許你會有疑惑,僅僅是因為CAS失敗,就獲取共享鎖失敗了嗎?而且,ReentrantReadWriteLock是一個可重入鎖,這裡也沒看到有重入的地方啊。

別急,如果step2失敗,會進入step3,到第三個紅框,進入fullTryAcquireShared(Thread current)方法。

2.3  final int fullTryAcquireShared(Thread current)

這個方法比較長,裡面用了for(;;) 自旋CAS,為什麼呢?因為CAS還是可能會失敗啊……失敗就得繼續再嘗試一把。

我就貼出for(;;) 裡的程式碼,分為兩段,第一段判斷是否可以嘗試獲取鎖(與上面類似,加了重入的判斷),第二段CAS和成功後的一些操作。

先看第一段,判斷是否可以嘗試獲取鎖。

step1. 如果有執行緒持有獨佔鎖,並且不是當前執行緒,返回失敗標誌-1。如果是當前執行緒,由於可重入的語義,通過了判斷,直接跑到第二段程式碼了。說明在持有獨佔鎖的情況下可以獲取共享鎖(鎖降級)。

step2. 如果當前沒有執行緒持有獨佔鎖,那麼再來看看熟悉的readerShouldBlock()。通過上面的分析我們知道,在公平模式下有節點在阻塞就得排隊,在非公平模式下有可能不可以搶在人家前面。為什麼是有可能?因為要考慮可重入的場景。

如果firstReader是當前執行緒,或者當前執行緒的cachedHoldCounter變數的count不為0(表示當前執行緒已經持有了該共享鎖),均說明當前執行緒已經持有共享鎖,此次獲取共享鎖是重入,這也是允許的,可以通過判斷。

如果可以順利通過上面兩步判斷,說明獲取共享鎖成功,下面開始熟悉的CAS。 

 

失敗了咋辦?別忘記是自旋啊,外層是for(;;),那就再來一發~~。當然還得再來一遍第一段的判斷。

3. 結論

經過上面的分析,可以來回答我的同事的問題了。

在Java程式設計中,當有一條執行緒要獲取ReentrantReadWriteLock的讀鎖,此時已經有其他執行緒獲得了讀鎖,AQS佇列裡也有執行緒在等待寫鎖。由於讀鎖是共享鎖,當前執行緒是馬上獲得讀鎖,還是排隊?如果是馬上獲得讀鎖,那豈不是阻塞的等待寫鎖的執行緒有可能一直(或長時間)拿不到寫鎖(寫鎖飢餓)?

1.如果已經有執行緒持有獨佔鎖

1.1 該執行緒不是當前執行緒,不用想了,乖乖排隊;

1.2 該執行緒就是當前執行緒,重入,CAS獲取共享鎖;

2.如果沒有執行緒持有獨佔鎖,檢查當前執行緒是否需要block(readerShouldBlock方法)。

block的判斷,有兩種模式,公平和非公平(預設模式)。如果不需要block, 必須滿足:公平模式下,沒有節點在AQS等待;非公平模式下,AQS第一個等待的節點不是獨佔式的;

2.1 不需要block,可以CAS獲取共享鎖;

2.2 需要block;

2.2.1 當前執行緒已經持有了共享鎖,重入,還是可以CAS獲取共享鎖;

2.2.2 當前執行緒前沒有已經持有共享鎖,則獲取失敗,只能排隊。

 

上面是根據程式碼邏輯整理的,可以換為更簡潔的語言。

如果當前執行緒已經持有獨佔鎖或共享鎖(重入)或不需要block,則CAS獲取共享鎖;否則,排隊。

在問題的場景中,當前執行緒並沒有獲取到寫鎖或讀鎖,不能重入;非公平模式下AQS中第一個等待的是想要獲取獨佔鎖的節點(公平模式不贅述),必須block,所以當前執行緒只能排隊,並不會出現阻塞的想獲取寫鎖的節點一直拿不到寫鎖的情況。

 為什麼知道等待寫鎖的節點一定是第一個等待的節點呢?因為如果它前面還有節點,獲取到讀鎖的節點會喚醒後面同樣等待讀鎖的節點(共享鎖的特點),所以等待寫鎖的節點,大概率是第一個節點(也有可能等待讀鎖的節點還沒被喚醒,當然這是瞬時發生的,這種場景應該小概率吧……)。我想,這也是readerShouldBlock()要判斷第一個等待節點的原因吧。

4. 舉個栗子

 1 package com.khlin.my.test;
 2 
 3 import java.util.concurrent.locks.ReentrantReadWriteLock;
 4 
 5 public class RRWLockTest {
 6 
 7     public static void main(String[] args) throws InterruptedException {
 8         final ReentrantReadWriteLock LOCK = new ReentrantReadWriteLock();
 9 
10         Thread reader1 = new Thread(new Runnable() {
11             public void run() {
12                 try {
13                     LOCK.readLock().lock();
14                     System.out.println("reader1 locked.");
15                     Thread.sleep(3000L);
16                     System.out.println("reader1 finished.");
17                 } catch (InterruptedException e) {
18                     e.printStackTrace();
19                 } finally {
20                     LOCK.readLock().unlock();
21                 }
22             }
23         });
24 
25         Thread reader2 = new Thread(new Runnable() {
26             public void run() {
27                 try {
28                     LOCK.readLock().lock();
29                     System.out.println("reader2 locked.");
30                     System.out.println("reader2 finished.");
31                 } finally {
32                     LOCK.readLock().unlock();
33                 }
34             }
35         });
36 
37         Thread writer = new Thread(new Runnable() {
38             public void run() {
39                 try{
40                     LOCK.writeLock().lock();
41                     System.out.println("writer locked.");
42                     System.out.println("writer finished.");
43                 }finally {
44                     LOCK.writeLock().unlock();
45                 }
46             }
47         });
48         reader1.start();
49         Thread.sleep(1000L);
50         writer.start();
51         Thread.sleep(1000L);
52         reader2.start();
53     }
54 }

reader1獲取了讀鎖,正在執行,隨後writer來獲取寫鎖,失敗,入隊等待。reader2由於writer正在等待(通過readerShouldBlock判斷),無法獲取讀鎖,入隊,等待。輸出如下:

如果把writer去掉,雖然reader1還沒執行完,但reader2可以馬上獲得讀鎖,無需等待。把上面第49,50行註釋掉,輸出如下:

 

相關文章