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行註釋掉,輸出如下: