Java鎖之ReentrantReadWriteLock

1、前言

上一篇Java鎖之ReentrantLock(二)分析了ReentrantLock實現利器AQS同步器,經過AQS源碼分析,咱們知道了同步器經過sate狀態進行鎖的獲取與釋放,同時構造了雙向FIFO雙向鏈表進行線程節點的等待,線程節點經過waitStatus來判斷本身須要掛起仍是喚醒去獲取鎖。那麼接下來咱們繼續分析ReentrantLock的讀寫鎖,ReentrantReadWriteLock鎖。java

2、ReentrantReadWriteLock總覽

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

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

咱們先分析讀寫鎖中的這4個int 常量,其實這4個常量的做用就是區分一個int整數的高16位和低16位的,ReentrantReadWriteLock鎖仍是依託於state變量做爲獲取鎖的標準,那麼一個state變量如何區分讀鎖和寫鎖呢?答案是經過位運算,高16位表示讀鎖,低16位表示寫鎖。若是對位運算不太熟悉或者不瞭解的同窗能夠看看這篇文章《位運算》。既然是分析讀寫鎖,那麼咱們先從讀鎖和寫鎖的源碼獲取入手分析。緩存

這裏先提早補充一個概念:併發

寫鎖和讀鎖是互斥的(這裏的互斥是指線程間的互斥,當前線程能夠獲取到寫鎖又獲取到讀鎖,可是獲取到了讀鎖不能繼續獲取寫鎖),這是由於讀寫鎖要保持寫操做的可見性,若是容許讀鎖在被獲取的狀況下對寫鎖的獲取,那麼正在運行的其餘讀線程沒法感知到當前寫線程的操做。所以,只有等待其餘線程都釋放了讀鎖,寫鎖才能被當前線程獲取,而一旦寫鎖被獲取,其餘讀寫線程的後續訪問都會被阻塞。源碼分析

  • 寫鎖tryLock()

咱們根據內部類WriteLock的調用關係找到源碼以下,發現最終寫鎖調用的是tryWriteLock()(以非阻塞獲取鎖方法爲例)post

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. 爲了保證線程間內容的可見性,讀鎖和寫鎖是互斥的,這裏的互斥是指線程間的互斥,當前線程能夠獲取到寫鎖又獲取到讀鎖,可是獲取到了讀鎖不能繼續獲取寫鎖。代理

3、Sync 同步器位運算分析

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

咱們再看看位運算的相關代碼(我假設你已經知道了位運算的相關基本知識,若是不具有,請閱讀《位運算》

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變量就知足了讀鎖和寫鎖的分離。

4、鎖降級

鎖降級是指寫鎖降級爲讀鎖。若是當前線程擁有寫鎖,而後將其釋放,最後再獲取讀鎖,這種分段完成的過程不能稱之爲鎖降級。鎖降級是指把持住(以前擁有的寫鎖的過程)源碼示例(來自於《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的鎖狀態進行爲運算,區分了讀狀態和寫狀態。
相關文章
相關標籤/搜索