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

