Java鎖之ReentrantReadWriteLock

木木匠發表於2018-08-26

一、前言

上一篇Java鎖之ReentrantLock(二)分析了ReentrantLock實現利器AQS同步器,通過AQS原始碼分析,我們知道了同步器通過sate狀態進行鎖的獲取與釋放,同時構造了雙向FIFO雙向連結串列進行執行緒節點的等待,執行緒節點通過waitStatus來判斷自己需要掛起還是喚醒去獲取鎖。那麼接下來我們繼續分析ReentrantLock的讀寫鎖,ReentrantReadWriteLock鎖。

二、ReentrantReadWriteLock總覽

ReentrantReadWriteLock鎖 實際也是繼承了AQS類來實現鎖的功能的,上一篇Java鎖之ReentrantLock(二)已經詳細解析過AQS的實現,如果已經掌握了AQS的原理,相信接下來的讀寫鎖的解析也非常容易。

  • ReentrantReadWriteLock鎖內部類列表
作用
Sync, 繼承AQS,鎖功能的主要實現者
FairSync 繼承Sync,主要實現公平鎖
NofairSync 繼承Sync,主要實現非公平鎖
ReadLock 讀鎖,通過sync代理實現鎖功能
WriteLock 寫鎖,通過sync代理實現鎖功能

Java鎖之ReentrantReadWriteLock

我們先分析讀寫鎖中的這4個int 常量,其實這4個常量的作用就是區分一個int整數的高16位和低16位的,ReentrantReadWriteLock鎖還是依託於state變數作為獲取鎖的標準,那麼一個state變數如何區分讀鎖和寫鎖呢?答案是通過位運算,高16位表示讀鎖,低16位表示寫鎖。如果對位運算不太熟悉或者不瞭解的同學可以看看這篇文章《位運算》。既然是分析讀寫鎖,那麼我們先從讀鎖和寫鎖的原始碼獲取入手分析。

這裡先提前補充一個概念:

寫鎖和讀鎖是互斥的(這裡的互斥是指執行緒間的互斥,當前執行緒可以獲取到寫鎖又獲取到讀鎖,但是獲取到了讀鎖不能繼續獲取寫鎖),這是因為讀寫鎖要保持寫操作的可見性,如果允許讀鎖在被獲取的情況下對寫鎖的獲取,那麼正在執行的其他讀執行緒無法感知到當前寫執行緒的操作。因此,只有等待其他執行緒都釋放了讀鎖,寫鎖才能被當前執行緒獲取,而一旦寫鎖被獲取,其他讀寫執行緒的後續訪問都會被阻塞。

  • 寫鎖tryLock()

我們根據內部類WriteLock的呼叫關係找到原始碼如下,發現最終寫鎖呼叫的是tryWriteLock()(以非阻塞獲取鎖方法為例)

 public boolean tryLock( ) {
            return sync.tryWriteLock();
        }
        
        
 final boolean tryWriteLock() {
            Thread current = Thread.currentThread();
            int c = getState();
            if (c != 0) {//狀態不等於0,說明已經鎖已經被獲取過了
                int w = exclusiveCount(c);//這裡是判斷是否獲取到了寫鎖,後面會詳細分析這段程式碼
                // 這裡就是判斷是否是鎖重入:2種情況
                // 1.c!=0說明是有鎖被獲取的,那麼w==0,
                // 說明寫鎖是沒有被獲取,也就是說讀鎖被獲取了,由於寫鎖和讀鎖的互斥,為了保證資料的可見性
                // 所以return false.
                //2. w!=0,寫鎖被獲取了,但是current != getExclusiveOwnerThread() ,
                // 說明是被別的執行緒獲取了,return false;
                if (w == 0 || current != getExclusiveOwnerThread())
                    return false;
                if (w == MAX_COUNT)//判斷是否溢位
                    throw new Error("Maximum lock count exceeded");
            }
            // 嘗試獲取鎖
            if (!compareAndSetState(c, c + 1))
                return false;
            setExclusiveOwnerThread(current);
            return true;
        }
複製程式碼
  • 讀鎖tryLock() 同樣我們先分析非阻塞獲取鎖方法,tryReadLock()
 final boolean tryReadLock() {
            Thread current = Thread.currentThread();
            for (;;) {
                int c = getState();
                if (exclusiveCount(c) != 0 &&
                    getExclusiveOwnerThread() != current)
                    return false; //寫鎖被其他執行緒獲取了,直接返回false
                int r = sharedCount(c); //獲取讀鎖的狀態
                if (r == MAX_COUNT)
                    throw new Error("Maximum lock count exceeded");
                if (compareAndSetState(c, c + SHARED_UNIT)) { //嘗試獲取讀鎖
                    if (r == 0) { //說明第一個獲取到了讀鎖
                        firstReader = current; //標記下當前執行緒是第一個獲取的
                        firstReaderHoldCount = 1; //重入次數
                    } else if (firstReader == current) {
                        firstReaderHoldCount++; //次數+1
                    } else {
                        //cachedHoldCounter 為快取最後一個獲取鎖的執行緒
                        HoldCounter rh = cachedHoldCounter;
                        if (rh == null || rh.tid != getThreadId(current))
                            cachedHoldCounter = rh = readHolds.get(); //快取最後一個獲取鎖的執行緒
                        else if (rh.count == 0)// 當前執行緒獲取到了鎖,但是重入次數為0,那麼把當前執行緒存入進去
                            readHolds.set(rh);
                        rh.count++;
                    }
                    return true;
                }
            }
        }
複製程式碼
  • 讀鎖的釋放tryReleaseShared()

寫鎖的釋放比較簡單,基本邏輯和讀鎖的釋放是一樣的,考慮到篇幅,這次主要分析讀鎖的釋放過程:

 protected final boolean tryReleaseShared(int unused) {
            Thread current = Thread.currentThread();
            if (firstReader == current) {
                // assert firstReaderHoldCount > 0;
                if (firstReaderHoldCount == 1)//如果是首次獲取讀鎖,那麼第一次獲取讀鎖釋放後就為空了
                    firstReader = null;
                else
                    firstReaderHoldCount--;
            } else {
                HoldCounter rh = cachedHoldCounter;
                if (rh == null || rh.tid != getThreadId(current))
                    rh = readHolds.get();
                int count = rh.count;
                if (count <= 1) { //表示全部釋放完畢
                    readHolds.remove();  //釋放完畢,那麼久把儲存的記錄次數remove掉
                    if (count <= 0)
                        throw unmatchedUnlockException();
                }
                --rh.count;
            }
            for (;;) {
                int c = getState();
                 // nextc 是 state 高 16 位減 1 後的值
                int nextc = c - SHARED_UNIT;
                if (compareAndSetState(c, nextc)) //CAS設定狀態
                    
                    return nextc == 0; //這個判斷如果高 16 位減 1 後的值==0,那麼就是讀狀態和寫狀態都釋放了
            }
        }
複製程式碼

上面就是讀寫鎖的獲取和釋放過程原始碼,先分析簡單的非阻塞獲取鎖方法,根據原始碼我們可以知道,寫鎖和讀鎖的是否獲取也是判斷狀態是否不為0,寫鎖的狀態獲取方法是exclusiveCount(c),讀鎖的狀態獲取方法是sharedCount(c)。那麼我們接下來分析下這兩個方法是如何對統一個變數位運算獲取各自的狀態的,在分析之前我們先小結下前面的內容。

  • 小結一下

a. 讀寫鎖依託於AQS的State變數的位運算來區分讀鎖和寫鎖,高16位表示讀鎖,低16位表示寫鎖。

b. 為了保證執行緒間內容的可見性,讀鎖和寫鎖是互斥的,這裡的互斥是指執行緒間的互斥,當前執行緒可以獲取到寫鎖又獲取到讀鎖,但是獲取到了讀鎖不能繼續獲取寫鎖。

三、Sync 同步器位運算分析

  • 狀態變數按照位劃分示意圖

Java鎖之ReentrantReadWriteLock

我們再看看位運算的相關程式碼(我假設你已經知道了位運算的相關基本知識,如果不具備,請閱讀《位運算》

        static final int SHARED_SHIFT   = 16;
        //實際是65536
        static final int SHARED_UNIT    = (1 << SHARED_SHIFT);
        //最大值 65535
        static final int MAX_COUNT      = (1 << SHARED_SHIFT) - 1;
        // 同樣是65535
        static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1;

        /** 獲取讀的狀態  */
        static int sharedCount(int c)    { return c >>> SHARED_SHIFT; }
        /** 獲取寫鎖的獲取狀態 */
        static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; }

複製程式碼

我們按照圖示內容的資料進行運算,圖示的32位二進位制資料為: 00000000000000100000000000000011

  • 讀狀態獲取

00000000000000100000000000000011 >>> 16,無符號右移16位,結果如下: 00000000000000000000000000000010,換算成10進位制數等於2,說明讀狀態為: 2

  • 讀狀態獲取

00000000000000100000000000000011 & 65535,轉換成2進位制運算為 00000000000000100000000000000011 & 00000000000000001111111111111111

最後與運算結果為: 00000000000000100000000000000011 ,換算成10進製為3

不得不佩服作者的思想,這種設計在不修改AQS的程式碼前提下,僅僅通過原來的State變數就滿足了讀鎖和寫鎖的分離。

四、鎖降級

鎖降級是指寫鎖降級為讀鎖。如果當前執行緒擁有寫鎖,然後將其釋放,最後再獲取讀鎖,這種分段完成的過程不能稱之為鎖降級。鎖降級是指把持住(之前擁有的寫鎖的過程)原始碼示例(來自於《java併發程式設計的藝術》):

public void processData(){
    readLock.lock();
    if(!update){
        //必須先釋放讀鎖
        readLock.unlock();
        //鎖降級從寫鎖獲取到開始
        writeLock.lock();
        try{
            if(!update){
                update =true;
            }
            readlock.lock();
        }finally{
            writeLock.unlock();
        }//鎖降級完成,寫鎖降級為讀鎖
    }
    try{
        //略
    }finally{
        readLock.unlock();
    }
}
複製程式碼

上述示例就是一個鎖降級的過程,需要注意的是update變數是一個volatie修飾的變數,所以,執行緒之間是可見的。該程式碼就是獲取到寫鎖後修改變數,然後獲取讀鎖,獲取成功後釋放寫鎖,完成了鎖的降級。注意:ReentrantReadWriteLock不支援鎖升級,這是因為如果多個執行緒獲取到了讀鎖,其中任何一個執行緒獲取到了寫鎖,修改了資料,其他的執行緒感知不到資料的更新,這樣就無法保證資料的可見性。

最後總結

  • 原始碼中,涉及了其他部分,本文做了精簡,比如:cachedHoldCounter,firstReader firstReaderHoldCount等屬性,這些屬性並沒有對理解原理有多少影響,主要是提升效能的作用,所以本文沒有討論。
  • 讀寫鎖還是依賴於AQS的自定義同步器來實現的,裡面的大部分程式碼和之前分析的兩篇文章《Java鎖之ReentrantLock》差不多,AQS的大部分解析已經在這兩篇文章已經解析過了,如果讀者對此還有疑惑的地方,可以看看這兩篇文章。
  • 讀寫鎖的巧妙設計,就是對AQS的鎖狀態進行為運算,區分了讀狀態和寫狀態。

相關文章