多線程7一ReentrantReadWriteLock源碼分析

前言

在多線程環境下,爲了保證線程安全, 我們通常會對共享資源加鎖操作,我們常用Synchronized關鍵字或者ReentrantLock 來實現,這兩者加鎖方式都是排他鎖,即同一時刻最多允許一個線程操作,然而大多數場景中對共享資源讀多於寫,那麼存在線程安全問題的是寫操作(修改,添加,刪除),我們是否應該考慮將讀和寫兩個分開,只要運用合理,併發性能是不是可以提高,吞吐量增大呢? ReentrantReadWriteLock已經爲我們實現了這種機制,我們一起來看它是怎樣實現的吧!

1、讀寫鎖的一些概念

在查看可重入讀寫鎖的源碼前,有幾個概念需要先知道,對於後面理解源碼很有幫助。

1、ReentrantReadWriteLock 內部 Sync類依然是繼承AQS實現的,因此同步狀態字段 state,依然表示對鎖資源的佔用情況。那麼如何實現一個 int類型的state 同時來表示讀寫鎖兩種狀態的佔用情況呢? 這裏實現非常巧妙,將4個字節的int類型, 32位拆分爲2部分,高16位表示讀鎖的佔用情況,低16位表示寫鎖的佔用情況,這樣讀寫鎖互不影響,相互獨立;也因此讀寫鎖的最大值是2^16-1 = 65535,不能超過16位,下面源碼有體現。

state值表示如圖所示:

在這裏插入圖片描述

2、讀鎖是共享鎖,只要不超過最大值,可多個線程同時獲取; 寫鎖是排他鎖,同一時刻最多允許一個線程獲取。

寫鎖與其他鎖都互斥,含寫寫互斥,寫讀互斥,讀寫互斥。

3、state可同時表示讀寫鎖的狀態,state的高16位表示獲取讀鎖的線程數,讀鎖支持可重入,即一個線程也可多次獲取讀鎖,怎麼維護每個讀鎖線程的重入次數的? 每個線程有一個計數器 HoldCounter,用ThreadLocal來存放每個線程的計數器;state的低16位表示寫鎖的同步狀態,因爲寫鎖是排他鎖,這裏就不能表示獲取寫鎖的線程數了,只能表示寫鎖的重入次數,獲取寫鎖的線程可多次重複獲取寫鎖(支持重入)。

讀鎖的計數器的實現原理如下:

可見ThreadLocalHoldCounter繼承 ThreadLocal,每個獲取讀鎖的線程是通過其本地變量來存儲自己的計數器,來統計獲取讀鎖的重入次數。ThreadLocal原理解析

    static final class ThreadLocalHoldCounter
            extends ThreadLocal<HoldCounter> {
          //重寫了ThreadLocal的initialValue方法
            public HoldCounter initialValue() {
                return new HoldCounter();
            }
        }

4、state的高16位需要記錄獲取讀鎖的線程數,每增加一個線程獲取讀鎖,在state的高16執行加1操作,即state+2^16,寫鎖增加重入次數,直接 state+1即可。

5、鎖降級:獲取寫鎖的線程,可以再次獲取到讀鎖,即寫鎖降級爲讀鎖。

​ 讀鎖可以升級爲寫鎖嗎? 不可以,因爲存在線程安全問題,試想獲取讀鎖的線程有多個,其中一個線程升級爲寫鎖,對臨界區資源進行操作,比如修改了某個值,對其他已經獲取讀鎖的線程不可見,出現線程安全問題。

代碼演示:

1、讀寫狀態

AQS(AbstractQueuedSynchronizer的簡稱)中同步狀態字段 private volatile int state, int類型,4個字節,32位,拆分爲高16位表示讀狀態,低16位表示寫狀態,如下定義了一些常量,實現獲取讀寫鎖的數量。

ReentrantReadWriteLock部分代碼如下:

   //分隔位數,16位 
     static final int SHARED_SHIFT   = 16;
   //讀鎖加1的數量,1左位移16位, (16)0x10000  = (2)1000000000000000= (10) 65536
        static final int SHARED_UNIT    = (1 << SHARED_SHIFT);
  //讀寫鎖的最大數量, (16)0xFFFFFFFF =(2)1111111111111111 =(10)65535 
        static final int MAX_COUNT      = (1 << SHARED_SHIFT) - 1;
  //寫鎖的掩碼,用於計算寫鎖重入次數時,將state的高16全部置爲0, 等於(2)1111111111111111
        static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1;
   //獲取讀鎖數,表示當前有多少個線程獲取到讀鎖
   static int sharedCount(int c)    { return c >>> SHARED_SHIFT; }
  //獲取寫鎖重入次數(不等於0表示有線程持有獨佔鎖,大於1,表示寫鎖有重入)
        static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; }

分別看一下獲取讀寫鎖數量的方法。

獲取佔用讀鎖的線程數,代碼如下:

 static int sharedCount(int c)    { return c >>> SHARED_SHIFT; }

傳入的c爲 state,state 無符號右移16位,抹去低16位值,左邊補0

示例圖如下:

在這裏插入圖片描述

獲取寫鎖的值的方法

  static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; }

與運算,將高16全部置爲0,低16值代表寫鎖的值,&運算,相同爲1,不同爲0,得到低16位寫鎖值。

示例圖如下:

在這裏插入圖片描述
2、三個鎖概念

  1. int c =getState() ,獲取state的值,代表同步鎖狀態,該值包含讀寫兩個鎖的同步狀態
  2. int w = exclusiveCount©; w代表寫鎖的同步狀態,通過c獲取到寫鎖的狀態值
  3. int r = sharedCount©; r 代表讀鎖的同步狀態,通過c獲取到讀鎖的狀態值

以下分析三種情況下state,r, w 的值及代表的含義:

  • 1、一個線程獲取到寫鎖:

state =1, w =1, r =0

獲取寫鎖加1操作就比較簡單了,因爲寫鎖是獨佔鎖,與正常的ReentrantLock獲取鎖實現一樣,佔用state的低16位表示,不用看state的高16,左邊補16位0。獲取寫鎖一次,直接 c+1;

  • 2、一個線程獲取到讀鎖:

state =65536, w= 0, r=1

c初始爲0 ,獲取讀鎖,則讀鎖數量+1,執行 c + SHARED_UNIT, SHARED_UNIT = (2)1000000000000000 = (10)65536,括號內表示進制,SHARED_UNIT是每次讀鎖加1的數值。

如下圖所示: 在獲取讀鎖數量 r時,將state的低16位抹去,r=1,而state此時的值= 2^16 =65536,state的實際值可能會很大,但其實分別拆分讀寫鎖的值不一定大,只是讀鎖值表示在高位,會造成state值很大。

在這裏插入圖片描述

  • 3、一個線程獲取到寫鎖,又獲取到讀鎖情況(鎖降級):

state = 65537,w=1, r=1

state二進制表示: 00000000 00000001 00000000 00000001

鎖降級代碼演示如下:

package readwritelock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
/**
 * @author zdd
 * 2019/12/30  上午
 * Description: 鎖降級測試
 */
public class ReadWriteLockTest {
    static Integer shareVar = 0;
    public static void main(String[] args) {
        ReentrantReadWriteLock rw = new ReentrantReadWriteLock();
        //1,首先獲取寫鎖
        rw.writeLock().lock();
        //2.修改共享變量值
        shareVar = 10 ;
        //3.再獲取讀鎖
        rw.readLock().lock();
        System.out.println("讀取變量值 shareVar:"+ shareVar);
        //4.釋放寫鎖
        rw.writeLock().unlock();
        //5.釋放讀鎖
        rw.readLock().unlock();
    }
}

2、類結構和構造方法

ReentrantReadWriteLock 類中有ReadLock和WriteLock,分別對應讀鎖和寫鎖,而讀寫鎖又分爲公平方式和非公平方式獲取鎖。

簡略類圖結構如下:

在這裏插入圖片描述

構造方法如下:根據傳入參數設置公平或者非公平獲取鎖方式,默認是非公平方式

  public ReentrantReadWriteLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
        readerLock = new ReadLock(this);
        writerLock = new WriteLock(this);
    }

3、寫鎖

由於寫鎖是獨佔鎖,獲取寫鎖的方式在AQS中已經說過了,詳見AQS源代碼分析, 只是每個子類的嘗試獲取鎖方式不同,所以ReentrantReadWriteLock類獲取寫鎖過程就看一下嘗試獲取鎖方法的源碼。

3.1、嘗試獲取鎖

tryAcquire(int acquires),獲取鎖失敗則加入同步隊列中等待獲取鎖,源代碼如下:

 protected final boolean tryAcquire(int acquires) {
            Thread current = Thread.currentThread();
           //1,獲取同步狀態state的值,注意該值可表示讀寫鎖的同步狀態
            int c = getState();
          //2,獲取寫鎖狀態,低16位的值
            int w = exclusiveCount(c);
          //3,如果同步鎖狀態不爲0,有線程已經獲取到了鎖 
            if (c != 0) {
        //4,w==0則表示寫鎖爲0,那麼一定有線程獲取了讀鎖,需要等待,讀寫互斥
 //current != getExclusiveOwnerThread() 當前線程不等於已經獲取到寫鎖的線程,則也需等待其釋放,寫寫互斥
                if (w == 0 || current != getExclusiveOwnerThread())
                    return false;
       //5,此時再次獲取鎖,判斷鎖重入次數是否超過最大限定次數
                if (w + exclusiveCount(acquires) > MAX_COUNT)
                    throw new Error("Maximum lock count exceeded");
                //更新寫鎖重入次數
                setState(c + acquires);
                return true;
            }
      //6,代碼執行這,一定是c==0,同步鎖空閒情況
     //writerShouldBlock該方法是基於公平鎖和非公平鎖2種方式的體現
            if (writerShouldBlock() ||
                !compareAndSetState(c, c + acquires))
                return false;
       //獲取到鎖,設置獨佔鎖爲當前寫鎖線程
            setExclusiveOwnerThread(current);
            return true;
        }

寫鎖是否應該阻塞等待

  • 1、 非公平鎖方式
  final boolean writerShouldBlock() {
          //直接返回false
            return false; // writers can always barge
        }
  • 2、公平鎖方式

需要判斷同步隊列中是否還有其他線程在掛起等待,如存在應該按照入隊順序獲取鎖

  final boolean writerShouldBlock() {
            return hasQueuedPredecessors();
        }
 public final boolean hasQueuedPredecessors() {
   //1.獲取同步隊列頭,尾節點
        Node t = tail; 
        Node h = head;
        Node s;
  // h !=t 同步隊列不爲空
  // 隊列中還有其他線程在等待鎖,則返回true
        return h != t &&
            ((s = h.next) == null || s.thread != Thread.currentThread());
    }
3.2、釋放寫鎖

unlock方法釋放鎖

public void unlock() {
    sync.release(1);
}

可見,調用內部類Sync的release方法,Sync繼承AQS

public final boolean release(int arg) {
    if (tryRelease(arg)) {
       //1,釋放鎖成功
        Node h = head;
        if (h != null && h.waitStatus != 0)
        //2.喚醒同步隊列中等待線程
            unparkSuccessor(h);
        return true;
    }
    return false;
}

核心在嘗試釋放鎖方法上,看看寫鎖的釋放鎖方法tryRelease

   protected final boolean tryRelease(int releases) {
           //1,判斷當前線程是否持有當前鎖
            if (!isHeldExclusively())
                throw new IllegalMonitorStateException();
            //2,同步狀態 - 需要釋放的寫鎖同步值
            int nextc = getState() - releases;
            boolean free = exclusiveCount(nextc) == 0;
            if (free)
              //3,free ==true,完全釋放寫鎖,將當前獲取獨佔鎖線程置空
                setExclusiveOwnerThread(null);
           //4,更新state值
            setState(nextc);
            return free;
        }

注: 在釋放寫鎖佔用次數時, state的高16的讀鎖有值也不影響,減去releases,首先減去的state低位的數,而且在釋放寫鎖時,state的低16位的值一定>=1,不存在減少讀鎖的值情況。

   int nextc = getState() - releases;
   boolean free = exclusiveCount(nextc) == 0;

也可改寫爲如下面代碼

//1,獲取state值
int c = getState();
//2,獲取寫鎖的值
int w= exclusiveCount(c);
int remain = w- releases;
boolean free = remain== 0;

4、讀鎖

4.1、獲取讀鎖

讀鎖調用lock方法加鎖,實際調用Sync的acquireShared方法

  public void lock() {
            sync.acquireShared(1);
        }

走進acquireShared,獲取共享鎖方法

 public final void acquireShared(int arg) {
        if (tryAcquireShared(arg) < 0)
            doAcquireShared(arg);
    }

嘗試獲取鎖tryAcquireShared,如果返回值<0, 表示獲取讀鎖失敗

主要執行步驟:

1、首先判斷是否存在其他線程在佔用寫鎖,有需要掛起等待;

2、在不用阻塞等待,且讀鎖值沒有超過最大值,cas更新成功了state的值,可以獲取到讀鎖,還會做以下事:

​ a. 第一個獲取讀鎖的,直接記錄線程對象和其重入獲取讀鎖的次數

​ b. 非第一個獲取讀鎖的,則獲取緩存計數器(cachedHoldCounter),其記錄上一次獲取讀鎖的線程,如果是同一個線程,則直接更新其計數器的重入次數,如果緩存計數器爲空或緩存計數器的線程不是當前獲取讀鎖的線程,則從當前線程本地變量中獲取自己的計數器,更新計數器的值

  protected final int tryAcquireShared(int unused) {
          //1,獲取當前線程對象
            Thread current = Thread.currentThread();
          //2,獲取同步鎖的值
            int c = getState();
         /*3,exclusiveCount(c) != 0 計算寫鎖的同步狀態,不等於0,說明有寫鎖已經獲取到同步鎖,
          *需要判斷當前線程是否等於獲取寫鎖線程,
          *是,可以允許再次獲取讀鎖,這裏涉及到鎖降級問題,寫鎖可以降爲讀鎖
          *否則不讓獲取,寫讀互斥
         */
            if (exclusiveCount(c) != 0 &&
                getExclusiveOwnerThread() != current)
                return -1;
       //4,獲取讀鎖同步狀態
            int r = sharedCount(c);
    /**
      *此處3個判斷條件
      * 1.是否應該阻塞等待,這裏也是基於公平鎖和非公平獲取鎖實現 
      * 2.讀鎖同步狀態值是超過最大值,即限制獲取讀鎖的最大線程數
      * 3.cas更新讀鎖同步狀態是否成功
      */
      if (!readerShouldBlock() &&
                r < MAX_COUNT &&
                compareAndSetState(c, c + SHARED_UNIT)) {
        //可以獲取到讀鎖
         //r==0表示是第一個獲取讀鎖的線程
                if (r == 0) {
                    firstReader = current;
                   //記錄第一個線程讀鎖的重入次數
                    firstReaderHoldCount = 1;
                } else if (firstReader == current) {
                  //是第一個獲取讀鎖線程,鎖重入,鎖重入次數+1
                    firstReaderHoldCount++;
                } else {
               // 已有其他線程獲取到讀鎖 
        /*
         *1,獲取緩存記錄的計數器,計數器是用來統計每一個獲取讀鎖線程的重入次數的,
         *由每個線程的ThreadLocal,即線程內的副本存儲,相互獨立;
         *此處也不是放入緩存,在有多個線程同時獲取讀鎖情況,
         *用一個變量記錄上一個獲取讀鎖的線程的計數器,可能考慮多次獲取讀鎖線程大概率是同一個線程情況,
         *這樣做是可提高執行效率
          */
                    HoldCounter rh = cachedHoldCounter;
          // rh==null,第一個獲取讀鎖,rh沒有值
   // 或者計數器存儲的上一次線程的id與當前線程不等, 即不是相同一個線程,
  //那麼就獲取當前線程內部的計數器,並賦值給cachedHoldCounter變量,這樣可以讓下一次獲取讀鎖線程獲取比較了
       if (rh == null || rh.tid != getThreadId(current))
              cachedHoldCounter = rh = readHolds.get();
         else if (rh.count == 0)
  /*進入該條件,我理解是在線程獲取讀鎖再釋放後,同一線程再次獲取讀鎖情況,
   * 緩存計數器會記錄上一個線程計數器,因爲線程釋放讀鎖後,count=0,
   * 這裏重新將計數器放入線程內部中,
   * 因爲線程在使用完線程內部變量後會防止內存泄漏,會執行remove,釋放本地存儲的計數器。
   */
                readHolds.set(rh);
        //計數器+1 
         rh.count++;
              }
                return 1;
            }
       //上面3個條件沒有同時滿足,沒有成功獲取到讀鎖,開始無限循環嘗試去獲取讀鎖
            return fullTryAcquireShared(current);
        }

無限循環嘗試獲取共享鎖 fullTryAcquireShared方法

主要執行步驟:

1、 如果有其他線程獲取到了寫鎖,寫讀互斥,應該去掛起等待;

2、如果可以獲取讀鎖,判斷是否應該阻塞等待,在公平獲取鎖方式中,同步隊列中有其他線程在等待,則應該去排隊按照FIFO順序獲取鎖,非公平獲取鎖方式,可以直接去競爭獲取鎖。

3、可以獲取鎖,則嘗試cas更新state的值,更新成功,獲取到鎖。

  final int fullTryAcquireShared(Thread current){
            HoldCounter rh = null;
        //無限循環
            for (;;) {
              //獲取同步鎖狀態
                int c = getState();
               //判斷寫鎖值不爲0,且不是當前線程,不可獲取讀鎖
                if (exclusiveCount(c) != 0) {
                    if (getExclusiveOwnerThread() != current)
                        return -1;
                } else if (readerShouldBlock()) {
               //沒有線程獲取到寫鎖情況,公平獲取鎖情況,
               //同步隊列中有其他線程等待鎖,該方法主要是在需要排隊等待,計數器重入次數==0情況,清除計數器
                    if (firstReader == current) {
               //此處firstReader !=null, 則第1個獲取讀鎖的線程還沒釋放鎖,可允許該線程繼續重入獲取鎖
               //計數器count一定>0
                    } else {
                        if (rh == null) {
                            rh = cachedHoldCounter;
                            if (rh == null || rh.tid != getThreadId(current)) {
                                rh = readHolds.get();
                                if (rh.count == 0)
                                  //清除計數器
                                    readHolds.remove();
                            }
                        }
              // 爲什麼rh.count == 0就不讓線程獲取到鎖了,基於公平獲取鎖方式,去同步隊列中等待
                        if (rh.count == 0)
                            return -1;
                    }
                }
                //獲取讀鎖線程超過最大限制值 65535
                if (sharedCount(c) == MAX_COUNT)
                    throw new Error("Maximum lock count exceeded");
               // cas執行讀鎖值+1
                if (compareAndSetState(c, c + SHARED_UNIT)) {
                    if (sharedCount(c) == 0) {
                      //1,第一個獲取讀鎖
                        firstReader = current;
                        firstReaderHoldCount = 1;
                    } else if (firstReader == current) {
                      //2,第一個獲取讀鎖重入
                        firstReaderHoldCount++;
                    } else {
                      //3,非第一個線程獲取讀鎖,存在多個線程獲取讀鎖
                        if (rh == null)
                            rh = cachedHoldCounter;
                        if (rh == null || rh.tid != getThreadId(current))
                            rh = readHolds.get();
                        else if (rh.count == 0)
                            readHolds.set(rh);
                        rh.count++;
                      //緩存計數器變量記錄此次獲取讀鎖線程的計數器
                        cachedHoldCounter = rh; // cache for release
                    }
                    return 1;
                }
            }
        }

tryAcquireShared 返回< 0, 獲取鎖失敗,執行 doAcquireShared

在獲取讀鎖失敗後,執行以下步驟:

1、將節點加入同步隊列中

2、如果前置節點是頭節點,將再次嘗試獲取鎖,如果成功,設置當前節點爲head節點,並根據tryAcquireShared方法的返回值r判斷是否需要繼續喚醒後繼節點,如果 r大於0,需要繼續喚醒後繼節點,r=0不需要喚醒後繼節點。

3、如果前置節點不是頭節點,則在隊列中找到安全位置,設置前置節點 ws=SIGNAL, 掛起等待。

private void doAcquireShared(int arg) {
        final Node node = addWaiter(Node.SHARED);
        boolean failed = true;
        try {
            boolean interrupted = false;
            for (;;) {
                final Node p = node.predecessor();
                if (p == head) {
                   //如果前繼節點是頭節點,再次嘗試獲取共享鎖
                    int r = tryAcquireShared(arg);
                   //r>=0,表示獲取到鎖, 
                   //r=0,表示不需要喚醒後繼節點
                   //r>0,需要繼續喚醒後繼節點
                    if (r >= 0) {
                       //該方法實現2個步驟
                       //1,設置當前節點爲頭節點
                       //2,r>0情況會繼續喚醒後繼節點
                        setHeadAndPropagate(node, r);
                      //舊的頭節點移出隊列
                        p.next = null; // help GC
                        if (interrupted)
                            selfInterrupt();
                        failed = false;
                        return;
                    }
                }
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

setHeadAndPropagate 該方法是與獨佔鎖獲取鎖的區別之處,獲取到鎖後,設置爲頭結點還需要繼續傳播下去。

private void setHeadAndPropagate(Node node, int propagate) {
   //記錄是的舊的頭節點
   Node h = head; // Record old head for check 
   //設置當前獲取到鎖節點爲頭節點
    setHead(node);
   //propagate >0,表示還需要繼續喚醒後繼節點
   //舊的頭節點和新頭節點爲空,或者ws<0,滿足條件之一,嘗試去喚醒後繼節點
    if (propagate > 0 || h == null || h.waitStatus < 0 ||
        (h = head) == null || h.waitStatus < 0) {
        Node s = node.next;
       //後繼節點爲空或者是共享節點(獲取讀鎖的線程)
        if (s == null || s.isShared())
            doReleaseShared();
    }
}

doReleaseShared 方法較難理解,在釋放鎖中也有調用,留着後面一起分析。

4.2、釋放讀鎖
public void unlock() {
    sync.releaseShared(1);
}

AQS中釋放共鎖方法releaseShared

public final boolean releaseShared(int arg) {
    if (tryReleaseShared(arg)) {
        doReleaseShared();
        return true;
    }
    return false;
}

看一下讀寫鎖具體實現tryReleaseShared 的方法

protected final boolean tryReleaseShared(int unused) {
    Thread current = Thread.currentThread();
  //1,更新或者移出線程內部計數器的值
    if (firstReader == current) {
        //當前線程是第一個獲取讀鎖的線程
        if (firstReaderHoldCount == 1)
          //直接置空
            firstReader = null;
        else
          //該線程獲取讀鎖重入多次,計數器-1
            firstReaderHoldCount--;
    } else {
        HoldCounter rh = cachedHoldCounter;
        if (rh == null || rh.tid != getThreadId(current))
            rh = readHolds.get();
        int count = rh.count;
        if (count <= 1) {
          //非第一個獲取讀鎖線程,避免ThreadLocal內存泄漏,移出計數器
            readHolds.remove();
            if (count <= 0)
             //此處是調用釋放鎖次數比獲取鎖次數還多情況,直接拋異常
                throw unmatchedUnlockException();
        }
        --rh.count;
    }
   //2,循環cas更新同步鎖的值
    for (;;) {
        int c = getState();
        //讀鎖同步狀態-1
        int nextc = c - SHARED_UNIT;
        if (compareAndSetState(c, nextc))
            // Releasing the read lock has no effect on readers,
            // but it may allow waiting writers to proceed if
            // both read and write locks are now free.
          //返回完全釋放讀鎖,讀鎖值是否==0,完全釋放,等待寫鎖線程可獲取
            return nextc == 0;
    }
}

tryReleaseShared 返回true情況,表示完全釋放讀鎖,執行doReleaseShared,那就需要喚醒同步隊列中等待的其他線程

在讀寫鎖中存在幾種情況

情況一、如果當前獲取鎖的線程佔用的是寫鎖,則後來無論是獲取讀鎖還寫鎖的線程都會被阻塞在同步隊列中,

同步隊列是FIFO隊列,在佔用寫鎖的釋放後,node1獲取讀鎖,因讀鎖是共享的,繼續喚醒後一個共享節點。

在這裏插入圖片描述

如上圖,在node1獲取到讀鎖時,會調用doReleaseShared方法,繼續喚醒下一個共享節點node2,可以持續將喚醒動作傳遞下去,如果node2後面還存在幾個等待獲取讀鎖的線程,這些線程是由誰喚醒的?是其前置節點,還是第一個獲取讀鎖的節點? 應該是第1個獲取鎖的節點,這裏即node1, 由下代碼可見,在無限循環中,只有頭節點沒有變化時,即再沒其他節點獲取到鎖後,纔會跳出循環。

private void doReleaseShared() {
    for (;;) {
      //獲取同步隊列中頭節點
        Node h = head;
      //同步隊列中節點不爲空,且節點數至少2個
        if (h != null && h != tail) {
            int ws = h.waitStatus;
            //1,表示後繼節點需要被喚醒
            if (ws == Node.SIGNAL) {
                if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
                    continue;            // loop to recheck cases
               //喚醒後繼節點 
                unparkSuccessor(h);
            }
           //2,後繼節點暫時不需要喚醒,設置節點 ws = -3, 確保後面可以繼續傳遞下去
            else if (ws == 0 &&
                     !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
                continue;                // loop on failed CAS
        }
        //如果頭節點發生變化,表示已經有其他線程獲取到鎖了,需要重新循環,確保可以將喚醒動作傳遞下去。
        if (h == head)                   // loop if head changed
            break;
    }
}

5、思考

1、在非公平獲取鎖方式下,是否存在等待獲取寫鎖的線程始終獲取不到鎖,每次都被後來獲取讀鎖的線程搶先,造成飢餓現象?

存在這種情況,從獲取讀鎖源碼中看出,如果第一個線程獲取到讀鎖正在執行情況下,第二個等待獲取寫鎖的線程在同步隊列中掛起等待,在第一個線程沒有釋放讀鎖情況下,又陸續來了線程獲取讀鎖,因爲讀鎖是共享的,線程都可以獲取到讀鎖,始終是在讀鎖沒有釋放完畢加入獲取讀鎖的線程,那麼等待獲取寫鎖的線程是始終拿不到寫鎖,導致飢餓。爲什麼默認還是非公平模式?因爲減少線程的上下文切換,保證更大的吞吐量。

6、總結

1、讀寫鎖可支持公平和非公平兩種方式獲取鎖。

2、支持鎖降級,寫鎖可降級爲讀鎖,但讀鎖不可升級爲寫鎖。

3、大多數場景是讀多於寫的,所以ReentrantReadWriteLock 比 ReentrantLock(排他鎖)有更好的併發性能和吞吐量。

4、讀寫鎖中讀鎖和寫鎖都支持鎖重入。

5、在獲取Condition對象實現阻塞喚醒機制,ReentrantReadWriteLock.WriteLock 重寫了 newCondition方法,ReadLock不支持,即讀鎖不支持與Condition配合使用,使用阻塞喚醒機制。


道阻且長,且歌且行!

每天一小步,踏踏實實走好腳下的路,文章爲自己學習總結,不復制黏貼,就是想讓自己的知識沉澱一下,也希望與更多的人交流,如有錯誤,請批評指正!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章