Java編程進階——Java 讀寫鎖 ReentrantReadWriteLock 源碼分析

本文內容:讀寫鎖 ReentrantReadWriteLock 的源碼分析,基於 Java7/Java8。

閱讀建議:雖然我這裡會介紹一些 AQS 的知識,不過如果你完全不瞭解 AQS,看本文就有點吃力了。

目錄

  • 使用示例
  • ReentrantReadWriteLock 總覽
  • 源碼分析
  • 讀鎖獲取
  • 讀鎖釋放
  • 寫鎖獲取
  • 寫鎖釋放
  • 鎖降級
  • 總結

使用示例

下面這個例子非常實用,我是 javadoc 的搬運工:


  1. // 這是一個關於緩存操作的故事
  2. class CachedData {
  3. Object data;
  4. volatile boolean cacheValid;
  5. // 讀寫鎖實例
  6. final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
  7. void processCachedData() {
  8. // 獲取讀鎖
  9. rwl.readLock().lock();
  10. if (!cacheValid) { // 如果緩存過期了,或者為 null
  11. // 釋放掉讀鎖,然後獲取寫鎖 (後面會看到,沒釋放掉讀鎖就獲取寫鎖,會發生死鎖情況)
  12. rwl.readLock().unlock();
  13. rwl.writeLock().lock();
  14. try {
  15. if (!cacheValid) { // 重新判斷,因為在等待寫鎖的過程中,可能前面有其他寫線程執行過了
  16. data = ...
  17. cacheValid = true;
  18. }
  19. // 獲取讀鎖 (持有寫鎖的情況下,是允許獲取讀鎖的,稱為 “鎖降級”,反之不行。)
  20. rwl.readLock().lock();
  21. } finally {
  22. // 釋放寫鎖,此時還剩一個讀鎖
  23. rwl.writeLock().unlock(); // Unlock write, still hold read
  24. }
  25. }
  26. try {
  27. use(data);
  28. } finally {
  29. // 釋放讀鎖
  30. rwl.readLock().unlock();
  31. }
  32. }
  33. }

ReentrantReadWriteLock 分為讀鎖和寫鎖兩個實例,讀鎖是共享鎖,可被多個線程同時使用,寫鎖是獨佔鎖。持有寫鎖的線程可以繼續獲取讀鎖,反之不行。

ReentrantReadWriteLock 總覽

這一節比較重要,我們要先看清楚 ReentrantReadWriteLock 的大框架,然後再到源碼細節。

首先,我們來看下 ReentrantReadWriteLock 的結構,它有好些嵌套類:

Java編程進階——Java 讀寫鎖 ReentrantReadWriteLock 源碼分析

大家先仔細看看這張圖中的信息。然後我們把 ReadLock 和 WriteLock 的代碼提出來一起看,清晰一些:

Java編程進階——Java 讀寫鎖 ReentrantReadWriteLock 源碼分析

很清楚了,ReadLock 和 WriteLock 中的方法都是通過 Sync 這個類來實現的。Sync 是 AQS 的子類,然後再派生了公平模式和不公平模式。

從它們調用的 Sync 方法,我們可以看到: ReadLock 使用了共享模式,WriteLock 使用了獨佔模式

等等,同一個 AQS 實例怎麼可以同時使用共享模式和獨佔模式???

這裡給大家回顧下 AQS,我們橫向對比下 AQS 的共享模式和獨佔模式:

Java編程進階——Java 讀寫鎖 ReentrantReadWriteLock 源碼分析

AQS 的精髓在於內部的屬性 state

  1. 對於獨佔模式來說,通常就是 0 代表可獲取鎖,1 代表鎖被別人獲取了,重入例外
  2. 而共享模式下,每個線程都可以對 state 進行加減操作

也就是說,獨佔模式和共享模式對於 state 的操作完全不一樣,那讀寫鎖 ReentrantReadWriteLock 中是怎麼使用 state 的呢?答案是將 state 這個 32 位的 int 值分為高 16 位和低 16位,分別用於共享模式和獨佔模式

源碼分析

有了前面的概念,大家心裡應該都有數了吧,下面就不再那麼囉嗦了,直接代碼分析。

源代碼加註釋 1500 行,並不算難,我們要看的代碼量不大。如果你前面一節都理解了,那麼直接從頭開始一行一行往下看就是了,還是比較簡單的。

ReentrantReadWriteLock 的前面幾行很簡單,我們往下滑到 Sync 類,先來看下它的所有的屬性:


  1. abstract static class Sync extends AbstractQueuedSynchronizer {
  2. // 下面這塊說的就是將 state 一分為二,高 16 位用於共享模式,低16位用於獨佔模式
  3. static final int SHARED_SHIFT = 16;
  4. static final int SHARED_UNIT = (1 << SHARED_SHIFT);
  5. static final int MAX_COUNT = (1 << SHARED_SHIFT) - 1;
  6. static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1;
  7. // 取 c 的高 16 位值,代表讀鎖的獲取次數(包括重入)
  8. static int sharedCount(int c) { return c >>> SHARED_SHIFT; }
  9. // 取 c 的低 16 位值,代表寫鎖的重入次數,因為寫鎖是獨佔模式
  10. static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; }
  11. // 這個嵌套類的實例用來記錄每個線程持有的讀鎖數量(讀鎖重入)
  12. static final class HoldCounter {
  13. // 持有的讀鎖數
  14. int count = 0;
  15. // 線程 id
  16. final long tid = getThreadId(Thread.currentThread());
  17. }
  18. // ThreadLocal 的子類
  19. static final class ThreadLocalHoldCounter
  20. extends ThreadLocal {
  21. public HoldCounter initialValue() {
  22. return new HoldCounter();
  23. }
  24. }
  25. /**
  26. * 組合使用上面兩個類,用一個 ThreadLocal 來記錄當前線程持有的讀鎖數量
  27. */
  28. private transient ThreadLocalHoldCounter readHolds;
  29. // 用於緩存,記錄"最後一個獲取讀鎖的線程"的讀鎖重入次數,
  30. // 所以不管哪個線程獲取到讀鎖後,就把這個值佔為已用,這樣就不用到 ThreadLocal 中查詢 map 了
  31. // 算不上理論的依據:通常讀鎖的獲取很快就會伴隨著釋放,
  32. // 顯然,在 獲取->釋放 讀鎖這段時間,如果沒有其他線程獲取讀鎖的話,此緩存就能幫助提高性能
  33. private transient HoldCounter cachedHoldCounter;
  34. // 第一個獲取讀鎖的線程(並且其未釋放讀鎖),以及它持有的讀鎖數量
  35. private transient Thread firstReader = null;
  36. private transient int firstReaderHoldCount;
  37. Sync() {
  38. // 初始化 readHolds 這個 ThreadLocal 屬性
  39. readHolds = new ThreadLocalHoldCounter();
  40. // 為了保證 readHolds 的內存可見性
  41. setState(getState()); // ensures visibility of readHolds
  42. }
  43. ...
  44. }
  45. state 的高 16 位代表讀鎖的獲取次數,包括重入次數,獲取到讀鎖一次加 1,釋放掉讀鎖一次減 1
  46. state 的低 16 位代表寫鎖的獲取次數,因為寫鎖是獨佔鎖,同時只能被一個線程獲得,所以它代表重入次數
  47. 每個線程都需要維護自己的 HoldCounter,記錄該線程獲取的讀鎖次數,這樣才能知道到底是不是讀鎖重入,用 ThreadLocal 屬性
    readHolds 維護
  48. cachedHoldCounter 有什麼用?其實沒什麼用,但能提示性能。將最後一次獲取讀鎖的線程的 HoldCounter 緩存到這裡,這樣比使用 ThreadLocal 性能要好一些,因為 ThreadLocal 內部是基於 map 來查詢的。但是 cachedHoldCounter 這一個屬性畢竟只能緩存一個線程,所以它要起提升性能作用的依據就是:通常讀鎖的獲取緊隨著就是該讀鎖的釋放。我這裡可能表達不太好,但是大家應該是懂的吧。
  49. firstReaderfirstReaderHoldCount 有什麼用?其實也沒什麼用,但是它也能提示性能。將"第一個"獲取讀鎖的線程記錄在 firstReader 屬性中,這裡的第一個不是全局的概念,等這個 firstReader 當前代表的線程釋放掉讀鎖以後,會有後來的線程佔用這個屬性的。firstReader 和 firstReaderHoldCount 使得在讀鎖不產生競爭的情況下,記錄讀鎖重入次數非常方便快速
  50. 如果一個線程使用了 firstReader,那麼它就不需要佔用 cachedHoldCounter
  51. 個人認為,讀寫鎖源碼中最讓初學者頭疼的就是這幾個用於提升性能的屬性了,使得大家看得雲裡霧裡的。主要是因為 ThreadLocal 內部是通過一個 ThreadLocalMap 來操作的,會增加檢索時間。而很多場景下,執行 unlock 的線程往往就是剛剛最後一次執行 lock 的線程,中間可能沒有其他線程進行 lock。還有就是很多不怎麼會發生讀鎖競爭的場景。

上面說了這麼多,是希望能幫大家降低後面閱讀源碼的壓力,大家也可以先看看後面的,然後再慢慢體會。

前面我們好像都只說讀鎖,完全沒提到寫鎖,主要是因為寫鎖真的是簡單很多,我也特地將寫鎖的源碼放到了後面,我們先啃下最難的讀鎖先。

讀鎖獲取

下面我就不一行一行按源碼順序說了,我們按照使用來說。

我們來看下讀鎖 ReadLock 的 lock 流程:


  1. // ReadLock
  2. public void lock() {
  3. sync.acquireShared(1);
  4. }
  5. // AQS
  6. public final void acquireShared(int arg) {
  7. if (tryAcquireShared(arg) < 0)
  8. doAcquireShared(arg);
  9. }

然後我們就會進到 Sync 類的 tryAcquireShared 方法:

在 AQS 中,如果 tryAcquireShared(arg) 方法返回值小於 0 代表沒有獲取到共享鎖(讀鎖),大於 0 代表獲取到

回顧 AQS 共享模式:tryAcquireShared 方法不僅僅在 acquireShared 的最開始被使用,這裡是 try,也就可能會失敗,如果失敗的話,執行後面的 doAcquireShared,進入到阻塞隊列,然後等待前驅節點喚醒。喚醒以後,還是會調用 tryAcquireShared 進行獲取共享鎖的。當然,喚醒以後再 try 是很容易獲得鎖的,因為這個節點已經排了很久的隊了,組織是會照顧它的。

所以,你在看下面這段代碼的時候,要想象到兩種獲取讀鎖的場景,一種是新來的,一種是排隊排到它的。


  1. protected final int tryAcquireShared(int unused) {
  2. Thread current = Thread.currentThread();
  3. int c = getState();
  4. // exclusiveCount(c) 不等於 0,說明有線程持有寫鎖,
  5. // 而且不是當前線程持有寫鎖,那麼當前線程獲取讀鎖失敗
  6. // (另,如果持有寫鎖的是當前線程,是可以繼續獲取讀鎖的)
  7. if (exclusiveCount(c) != 0 &&
  8. getExclusiveOwnerThread() != current)
  9. return -1;
  10. // 讀鎖的獲取次數
  11. int r = sharedCount(c);
  12. // 讀鎖獲取是否需要被阻塞,稍後細說。為了進去下面的分支,假設這裡不阻塞就好了
  13. if (!readerShouldBlock() &&
  14. // 判斷是否會溢出 (2^16-1,沒那麼容易溢出的)
  15. r < MAX_COUNT &&
  16. // 下面這行 CAS 是將 state 屬性的高 16 位加 1,低 16 位不變,如果成功就代表獲取到了讀鎖
  17. compareAndSetState(c, c + SHARED_UNIT)) {
  18. // =======================
  19. // 進到這裡就是獲取到了讀鎖
  20. // =======================
  21. if (r == 0) {
  22. // r == 0 說明此線程是第一個獲取讀鎖的,或者說在它前面獲取讀鎖的都走光光了,它也算是第一個吧
  23. // 記錄 firstReader 為當前線程,及其持有的讀鎖數量:1
  24. firstReader = current;
  25. firstReaderHoldCount = 1;
  26. } else if (firstReader == current) {
  27. // 進來這裡,說明是 firstReader 重入獲取讀鎖(這非常簡單,count 加 1 結束)
  28. firstReaderHoldCount++;
  29. } else {
  30. // 前面我們說了 cachedHoldCounter 用於緩存最後一個獲取讀鎖的線程
  31. // 如果 cachedHoldCounter 緩存的不是當前線程,設置為緩存當前線程的 HoldCounter
  32. HoldCounter rh = cachedHoldCounter;
  33. if (rh == null || rh.tid != getThreadId(current))
  34. cachedHoldCounter = rh = readHolds.get();
  35. else if (rh.count == 0)
  36. // 到這裡,那麼就是 cachedHoldCounter 緩存的是當前線程,但是 count 為 0,
  37. // 大家可以思考一下:這裡為什麼要 set ThreadLocal 呢?(當然,答案肯定不在這塊代碼中)
  38. // 既然 cachedHoldCounter 緩存的是當前線程,
  39. // 當前線程肯定調用過 readHolds.get() 進行初始化 ThreadLocal
  40. readHolds.set(rh);
  41. // count 加 1
  42. rh.count++;
  43. }
  44. // return 大於 0 的數,代表獲取到了共享鎖
  45. return 1;
  46. }
  47. // 往下看
  48. return fullTryAcquireShared(current);
  49. }

上面的代碼中,要進入 if 分支,需要滿足:readerShouldBlock() 返回 false,並且 CAS 要成功(我們先不要糾結 MAX_COUNT 溢出)。

那我們反向推,怎麼樣進入到最後的 fullTryAcquireShared:

  • readerShouldBlock() 返回 true,2 種情況:
  • 在 FairSync 中說的是 hasQueuedPredecessors(),即阻塞隊列中有其他元素在等待鎖。

也就是說,公平模式下,有人在排隊呢,你新來的不能直接獲取鎖

  • 在 NonFairSync 中說的是 apparentlyFirstQueuedIsExclusive(),即判斷阻塞隊列中 head 的第一個後繼節點是否是來獲取寫鎖的,如果是的話,讓這個寫鎖先來,避免寫鎖飢餓。

作者給寫鎖定義了更高的優先級,所以如果碰上獲取寫鎖的線程馬上就要獲取到鎖了,獲取讀鎖的線程不應該和它搶。

如果 head.next 不是來獲取寫鎖的,那麼可以隨便搶,因為是非公平模式,大家比比 CAS 速度

  • compareAndSetState(c, c + SHARED_UNIT) 這裡 CAS 失敗,存在競爭。可能是和另一個讀鎖獲取競爭,當然也可能是和另一個寫鎖獲取操作競爭。

然後就會來到 fullTryAcquireShared 中再次嘗試:


  1. /**
  2. * 1. 剛剛我們說了可能是因為 CAS 失敗,如果就此返回,那麼就要進入到阻塞隊列了,
  3. * 想想有點不甘心,因為都已經滿足了 !readerShouldBlock(),也就是說本來可以不用到阻塞隊列的,
  4. * 所以進到這個方法其實是增加 CAS 成功的機會
  5. * 2. 在 NonFairSync 情況下,雖然 head.next 是獲取寫鎖的,我知道它等待很久了,我沒想和它搶,
  6. * 可是如果我是來重入讀鎖的,那麼只能表示對不起了
  7. */
  8. final int fullTryAcquireShared(Thread current) {
  9. HoldCounter rh = null;
  10. // 別忘了這外層有個 for 循環
  11. for (;;) {
  12. int c = getState();
  13. // 如果其他線程持有了寫鎖,自然這次是獲取不到讀鎖了,乖乖到阻塞隊列排隊吧
  14. if (exclusiveCount(c) != 0) {
  15. if (getExclusiveOwnerThread() != current)
  16. return -1;
  17. // else we hold the exclusive lock; blocking here
  18. // would cause deadlock.
  19. } else if (readerShouldBlock()) {
  20. /**
  21. * 進來這裡,說明:
  22. * 1. exclusiveCount(c) == 0:寫鎖沒有被佔用
  23. * 2. readerShouldBlock() 為 true,說明阻塞隊列中有其他線程在等待
  24. *
  25. * 既然 should block,那進來這裡是幹什麼的呢?
  26. * 答案:是進來處理讀鎖重入的!
  27. *
  28. */
  29. // firstReader 線程重入讀鎖,直接到下面的 CAS
  30. if (firstReader == current) {
  31. // assert firstReaderHoldCount > 0;
  32. } else {
  33. if (rh == null) {
  34. rh = cachedHoldCounter;
  35. if (rh == null || rh.tid != getThreadId(current)) {
  36. // cachedHoldCounter 緩存的不是當前線程
  37. // 那麼到 ThreadLocal 中獲取當前線程的 HoldCounter
  38. // 如果當前線程從來沒有初始化過 ThreadLocal 中的值,get() 會執行初始化
  39. rh = readHolds.get();
  40. // 如果發現 count == 0,也就是說,純屬上一行代碼初始化的,那麼執行 remove
  41. // 然後往下兩三行,乖乖排隊去
  42. if (rh.count == 0)
  43. readHolds.remove();
  44. }
  45. }
  46. if (rh.count == 0)
  47. // 排隊去。
  48. return -1;
  49. }
  50. /**
  51. * 這塊代碼我看了蠻久才把握好它是幹嘛的,原來只需要知道,它是處理重入的就可以了。
  52. * 就是為了確保讀鎖重入操作能成功,而不是被塞到阻塞隊列中等待
  53. *
  54. * 另一個信息就是,這裡對於 ThreadLocal 變量 readHolds 的處理:
  55. * 如果 get() 後發現 count == 0,居然會做 remove() 操作,
  56. * 這行代碼對於理解其他代碼是有幫助的
  57. */
  58. }
  59. if (sharedCount(c) == MAX_COUNT)
  60. throw new Error("Maximum lock count exceeded");
  61. if (compareAndSetState(c, c + SHARED_UNIT)) {
  62. // 這裡 CAS 成功,那麼就意味著成功獲取讀鎖了
  63. // 下面需要做的是設置 firstReader 或 cachedHoldCounter
  64. if (sharedCount(c) == 0) {
  65. // 如果發現 sharedCount(c) 等於 0,就將當前線程設置為 firstReader
  66. firstReader = current;
  67. firstReaderHoldCount = 1;
  68. } else if (firstReader == current) {
  69. firstReaderHoldCount++;
  70. } else {
  71. // 下面這幾行,就是將 cachedHoldCounter 設置為當前線程
  72. if (rh == null)
  73. rh = cachedHoldCounter;
  74. if (rh == null || rh.tid != getThreadId(current))
  75. rh = readHolds.get();
  76. else if (rh.count == 0)
  77. readHolds.set(rh);
  78. rh.count++;
  79. cachedHoldCounter = rh;
  80. }
  81. // 返回大於 0 的數,代表獲取到了讀鎖
  82. return 1;
  83. }
  84. }
  85. }

firstReader 是每次將讀鎖獲取次數從 0 變為 1 的那個線程。

能緩存到 firstReader 中就不要緩存到 cachedHoldCounter 中。

上面的源碼分析應該說得非常詳細了,如果到這裡你不太能看懂上面的有些地方的註釋,那麼可以先往後看,然後再多看幾遍。

讀鎖釋放

下面我們看看讀鎖釋放的流程:


  1. // ReadLock
  2. public void unlock() {
  3. sync.releaseShared(1);
  4. }
  5. // Sync
  6. public final boolean releaseShared(int arg) {
  7. if (tryReleaseShared(arg)) {
  8. doReleaseShared(); // 這句代碼其實喚醒 獲取寫鎖的線程,往下看就知道了
  9. return true;
  10. }
  11. return false;
  12. }
  13. // Sync
  14. protected final boolean tryReleaseShared(int unused) {
  15. Thread current = Thread.currentThread();
  16. if (firstReader == current) {
  17. if (firstReaderHoldCount == 1)
  18. // 如果等於 1,那麼這次解鎖後就不再持有鎖了,把 firstReader 置為 null,給後來的線程用
  19. // 為什麼不順便設置 firstReaderHoldCount = 0?因為沒必要,其他線程使用的時候自己會設值
  20. firstReader = null;
  21. else
  22. firstReaderHoldCount--;
  23. } else {
  24. // 判斷 cachedHoldCounter 是否緩存的是當前線程,不是的話要到 ThreadLocal 中取
  25. HoldCounter rh = cachedHoldCounter;
  26. if (rh == null || rh.tid != getThreadId(current))
  27. rh = readHolds.get();
  28. int count = rh.count;
  29. if (count <= 1) {
  30. // 這一步將 ThreadLocal remove 掉,防止內存洩漏。因為已經不再持有讀鎖了
  31. readHolds.remove();
  32. if (count <= 0)
  33. // 就是那種,lock() 一次,unlock() 好幾次的逗比
  34. throw unmatchedUnlockException();
  35. }
  36. // count 減 1
  37. --rh.count;
  38. }
  39. for (;;) {
  40. int c = getState();
  41. // nextc 是 state 高 16 位減 1 後的值
  42. int nextc = c - SHARED_UNIT;
  43. if (compareAndSetState(c, nextc))
  44. // 如果 nextc == 0,那就是 state 全部 32 位都為 0,也就是讀鎖和寫鎖都空了
  45. // 此時這裡返回 true 的話,其實是幫助喚醒後繼節點中的獲取寫鎖的線程
  46. return nextc == 0;
  47. }
  48. }

讀鎖釋放的過程還是比較簡單的,主要就是將 hold count 減 1,如果減到 0 的話,還要將 ThreadLocal 中的 remove 掉。

然後是在 for 循環中將 state 的高 16 位減 1,如果發現讀鎖和寫鎖都釋放光了,那麼喚醒後繼的獲取寫鎖的線程。

寫鎖獲取

  1. 寫鎖是獨佔鎖。
  2. 如果有讀鎖被佔用,寫鎖獲取是要進入到阻塞隊列中等待的。
  3. // WriteLock
  4. public void lock() {
  5. sync.acquire(1);
  6. }
  7. // AQS
  8. public final void acquire(int arg) {
  9. if (!tryAcquire(arg) &&
  10. // 如果 tryAcquire 失敗,那麼進入到阻塞隊列等待
  11. acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
  12. selfInterrupt();
  13. }
  14. // Sync
  15. protected final boolean tryAcquire(int acquires) {
  16. Thread current = Thread.currentThread();
  17. int c = getState();
  18. int w = exclusiveCount(c);
  19. if (c != 0) {
  20. // 看下這裡返回 false 的情況:
  21. // c != 0 && w == 0: 寫鎖可用,但是有線程持有讀鎖(也可能是自己持有)
  22. // c != 0 && w !=0 && current != getExclusiveOwnerThread(): 其他線程持有寫鎖
  23. // 也就是說,只要有讀鎖或寫鎖被佔用,這次就不能獲取到寫鎖
  24. if (w == 0 || current != getExclusiveOwnerThread())
  25. return false;
  26. if (w + exclusiveCount(acquires) > MAX_COUNT)
  27. throw new Error("Maximum lock count exceeded");
  28. // 這裡不需要 CAS,仔細看就知道了,能到這裡的,只可能是寫鎖重入,不然在上面的 if 就攔截了
  29. setState(c + acquires);
  30. return true;
  31. }
  32. // 如果寫鎖獲取不需要 block,那麼進行 CAS,成功就代表獲取到了寫鎖
  33. if (writerShouldBlock() ||
  34. !compareAndSetState(c, c + acquires))
  35. return false;
  36. setExclusiveOwnerThread(current);
  37. return true;
  38. }

下面看一眼 writerShouldBlock() 的判定,然後你再回去看一篇寫鎖獲取過程。


  1. static final class NonfairSync extends Sync {
  2. // 如果是非公平模式,那麼 lock 的時候就可以直接用 CAS 去搶鎖,搶不到再排隊
  3. final boolean writerShouldBlock() {
  4. return false; // writers can always barge
  5. }
  6. ...
  7. }
  8. static final class FairSync extends Sync {
  9. final boolean writerShouldBlock() {
  10. // 如果是公平模式,那麼如果阻塞隊列有線程等待的話,就乖乖去排隊
  11. return hasQueuedPredecessors();
  12. }
  13. ...
  14. }

寫鎖釋放


  1. // WriteLock
  2. public void unlock() {
  3. sync.release(1);
  4. }
  5. // AQS
  6. public final boolean release(int arg) {
  7. // 1. 釋放鎖
  8. if (tryRelease(arg)) {
  9. // 2. 如果獨佔鎖釋放"完全",喚醒後繼節點
  10. Node h = head;
  11. if (h != null && h.waitStatus != 0)
  12. unparkSuccessor(h);
  13. return true;
  14. }
  15. return false;
  16. }
  17. // Sync
  18. // 釋放鎖,是線程安全的,因為寫鎖是獨佔鎖,具有排他性
  19. // 實現很簡單,state 減 1 就是了
  20. protected final boolean tryRelease(int releases) {
  21. if (!isHeldExclusively())
  22. throw new IllegalMonitorStateException();
  23. int nextc = getState() - releases;
  24. boolean free = exclusiveCount(nextc) == 0;
  25. if (free)
  26. setExclusiveOwnerThread(null);
  27. setState(nextc);
  28. // 如果 exclusiveCount(nextc) == 0,也就是說包括重入的,所有的寫鎖都釋放了,
  29. // 那麼返回 true,這樣會進行喚醒後繼節點的操作。
  30. return free;
  31. }

看到這裡,是不是發現寫鎖相對於讀鎖來說要簡單很多。

鎖降級

Doug Lea 沒有說寫鎖更高級,如果有線程持有讀鎖,那麼寫鎖獲取也需要等待。

不過從源碼中也可以看出,確實會給寫鎖一些特殊照顧,如非公平模式下,為了提高吞吐量,lock 的時候會先 CAS 競爭一下,能成功就代表讀鎖獲取成功了,但是如果發現 head.next 是獲取寫鎖的線程,就不會去做 CAS 操作。

Doug Lea 將持有寫鎖的線程,去獲取讀鎖的過程稱為鎖降級(Lock downgrading)。這樣,此線程就既持有寫鎖又持有讀鎖。

但是,鎖升級是不可以的。線程持有讀鎖的話,在沒釋放的情況下不能去獲取寫鎖,因為會發生死鎖

回去看下寫鎖獲取的源碼:


  1. protected final boolean tryAcquire(int acquires) {
  2. Thread current = Thread.currentThread();
  3. int c = getState();
  4. int w = exclusiveCount(c);
  5. if (c != 0) {
  6. // 看下這裡返回 false 的情況:
  7. // c != 0 && w == 0: 寫鎖可用,但是有線程持有讀鎖(也可能是自己持有)
  8. // c != 0 && w !=0 && current != getExclusiveOwnerThread(): 其他線程持有寫鎖
  9. // 也就是說,只要有讀鎖或寫鎖被佔用,這次就不能獲取到寫鎖
  10. if (w == 0 || current != getExclusiveOwnerThread())
  11. return false;
  12. ...
  13. }
  14. ...
  15. }

仔細想想,如果線程 a 先獲取了讀鎖,然後獲取寫鎖,那麼線程 a 就到阻塞隊列休眠了,自己把自己弄休眠了,而且可能之後就沒人去喚醒它了。

總結

關注筆者,私信回覆‘資料’獲取。

Java編程進階——Java 讀寫鎖 ReentrantReadWriteLock 源碼分析


分享到:


相關文章: