簡單分析線程獲取ReentrantReadWriteLock 讀鎖的規則


 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獲取共享鎖;否則,排隊。

readerShouldBlock()判斷第一個節點是獲取共享鎖或獨占鎖,在不考慮重入的情況下,是什么意思呢?

1. 第一個節點是等待獨占鎖的場景,說明下一個就是它了,不能搶它的,搶不到;

2. 第一個節點是等待共享鎖的場景,說明第一個節點,

2.1 在等待持有獨占鎖的線程釋放獨占鎖,這種必然是搶不到的。

2.2 持有共享鎖的線程還在喚醒后續節點的過程中,允許你去搶一下。當然,不意味着一定可以搶成功。

如果是2.2持有共享鎖的線程在喚醒后續節點過程中,理論上是可能獲取得到的。這種情況概率較小,我沒重現過。

回到這個問題。當前線程並沒有獲取到寫鎖或讀鎖,不能重入;AQS中,第一個等待的大概率是想要獲取獨占鎖的節點,必須block,所以當前線程只能排隊,並不會出現阻塞的想獲取寫鎖的節點一直拿不到寫鎖的情況;如果剛好沒有完全喚醒,那么可能是可以搶占的。但也不會一直阻塞,因為喚醒節點獲取讀鎖的過程是很快的。

總之,獲取讀鎖的機制,記住這個結論就行。

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

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判斷),無法獲取讀鎖,入隊,等待。輸出如下:

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM