Java鎖之ReentrantReadWriteLock

一、前言

上一篇Java鎖之ReentrantLock(二)分析了ReentrantLock實現(xiàn)利器AQS同步器搀突,通過AQS源碼分析蛀恩,我們知道了同步器通過sate狀態(tài)進行鎖的獲取與釋放谢鹊,同時構(gòu)造了雙向FIFO雙向鏈表進行線程節(jié)點的等待,線程節(jié)點通過waitStatus來判斷自己需要掛起還是喚醒去獲取鎖哥桥。那么接下來我們繼續(xù)分析ReentrantLock的讀寫鎖,ReentrantReadWriteLock鎖激涤。

二拟糕、ReentrantReadWriteLock總覽

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

  • ReentrantReadWriteLock鎖內(nèi)部類列表
作用
Sync, 繼承AQS犁嗅,鎖功能的主要實現(xiàn)者
FairSync 繼承Sync边涕,主要實現(xiàn)公平鎖
NofairSync 繼承Sync,主要實現(xiàn)非公平鎖
ReadLock 讀鎖愧哟,通過sync代理實現(xiàn)鎖功能
WriteLock 寫鎖奥吩,通過sync代理實現(xiàn)鎖功能
image

我們先分析讀寫鎖中的這4個int 常量,其實這4個常量的作用就是區(qū)分一個int整數(shù)的高16位和低16位的蕊梧,ReentrantReadWriteLock鎖還是依托于state變量作為獲取鎖的標準霞赫,那么一個state變量如何區(qū)分讀鎖和寫鎖呢?答案是通過位運算肥矢,高16位表示讀鎖端衰,低16位表示寫鎖。如果對位運算不太熟悉或者不了解的同學可以看看這篇文章《位運算》甘改。既然是分析讀寫鎖旅东,那么我們先從讀鎖和寫鎖的源碼獲取入手分析。

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

寫鎖和讀鎖是互斥的(這里的互斥是指線程間的互斥十艾,當前線程可以獲取到寫鎖又獲取到讀鎖抵代,但是獲取到了讀鎖不能繼續(xù)獲取寫鎖),這是因為讀寫鎖要保持寫操作的可見性忘嫉,如果允許讀鎖在被獲取的情況下對寫鎖的獲取荤牍,那么正在運行的其他讀線程無法感知到當前寫線程的操作。因此庆冕,只有等待其他線程都釋放了讀鎖康吵,寫鎖才能被當前線程獲取,而一旦寫鎖被獲取访递,其他讀寫線程的后續(xù)訪問都會被阻塞晦嵌。

  • 寫鎖tryLock()

我們根據(jù)內(nèi)部類WriteLock的調(diào)用關(guān)系找到源碼如下,發(fā)現(xiàn)最終寫鎖調(diào)用的是tryWriteLock()(以非阻塞獲取鎖方法為例)

 public boolean tryLock( ) {
            return sync.tryWriteLock();
        }
        
        
 final boolean tryWriteLock() {
            Thread current = Thread.currentThread();
            int c = getState();
            if (c != 0) {//狀態(tài)不等于0拷姿,說明已經(jīng)鎖已經(jīng)被獲取過了
                int w = exclusiveCount(c);//這里是判斷是否獲取到了寫鎖惭载,后面會詳細分析這段代碼
                // 這里就是判斷是否是鎖重入:2種情況
                // 1.c!=0說明是有鎖被獲取的,那么w==0跌前,
                // 說明寫鎖是沒有被獲取棕兼,也就是說讀鎖被獲取了,由于寫鎖和讀鎖的互斥抵乓,為了保證數(shù)據(jù)的可見性
                // 所以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); //獲取讀鎖的狀態(tài)
                if (r == MAX_COUNT)
                    throw new Error("Maximum lock count exceeded");
                if (compareAndSetState(c, c + SHARED_UNIT)) { //嘗試獲取讀鎖
                    if (r == 0) { //說明第一個獲取到了讀鎖
                        firstReader = current; //標記下當前線程是第一個獲取的
                        firstReaderHoldCount = 1; //重入次數(shù)
                    } else if (firstReader == current) {
                        firstReaderHoldCount++; //次數(shù)+1
                    } else {
                        //cachedHoldCounter 為緩存最后一個獲取鎖的線程
                        HoldCounter rh = cachedHoldCounter;
                        if (rh == null || rh.tid != getThreadId(current))
                            cachedHoldCounter = rh = readHolds.get(); //緩存最后一個獲取鎖的線程
                        else if (rh.count == 0)// 當前線程獲取到了鎖田弥,但是重入次數(shù)為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();  //釋放完畢只泼,那么久把保存的記錄次數(shù)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設(shè)置狀態(tài)
                    
                    return nextc == 0; //這個判斷如果高 16 位減 1 后的值==0剖笙,那么就是讀狀態(tài)和寫狀態(tài)都釋放了
            }
        }

上面就是讀寫鎖的獲取和釋放過程源碼,先分析簡單的非阻塞獲取鎖方法请唱,根據(jù)源碼我們可以知道弥咪,寫鎖和讀鎖的是否獲取也是判斷狀態(tài)是否不為0,寫鎖的狀態(tài)獲取方法是exclusiveCount(c),讀鎖的狀態(tài)獲取方法是sharedCount(c)十绑。那么我們接下來分析下這兩個方法是如何對統(tǒng)一個變量位運算獲取各自的狀態(tài)的聚至,在分析之前我們先小結(jié)下前面的內(nèi)容。

  • 小結(jié)一下

a. 讀寫鎖依托于AQS的State變量的位運算來區(qū)分讀鎖和寫鎖本橙,高16位表示讀鎖扳躬,低16位表示寫鎖。

b. 為了保證線程間內(nèi)容的可見性甚亭,讀鎖和寫鎖是互斥的坦报,這里的互斥是指線程間的互斥,當前線程可以獲取到寫鎖又獲取到讀鎖狂鞋,但是獲取到了讀鎖不能繼續(xù)獲取寫鎖。

三潜的、Sync 同步器位運算分析

  • 狀態(tài)變量按照位劃分示意圖
image

我們再看看位運算的相關(guān)代碼(我假設(shè)你已經(jīng)知道了位運算的相關(guān)基本知識骚揍,如果不具備,請閱讀《位運算》

        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;

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

我們按照圖示內(nèi)容的數(shù)據(jù)進行運算啰挪,圖示的32位二進制數(shù)據(jù)為:
00000000000000100000000000000011

  • 讀狀態(tài)獲取

00000000000000100000000000000011 >>> 16,無符號右移16位信不,結(jié)果如下:
00000000000000000000000000000010,換算成10進制數(shù)等于2,說明讀狀態(tài)為: 2

  • 讀狀態(tài)獲取

00000000000000100000000000000011 & 65535亡呵,轉(zhuǎn)換成2進制運算為
00000000000000100000000000000011 & 00000000000000001111111111111111

最后與運算結(jié)果為:
00000000000000100000000000000011 抽活,換算成10進制為3

不得不佩服作者的思想,這種設(shè)計在不修改AQS的代碼前提下锰什,僅僅通過原來的State變量就滿足了讀鎖和寫鎖的分離下硕。

四丁逝、鎖降級

鎖降級是指寫鎖降級為讀鎖。如果當前線程擁有寫鎖梭姓,然后將其釋放霜幼,最后再獲取讀鎖,這種分段完成的過程不能稱之為鎖降級誉尖。鎖降級是指把持鬃锛取(之前擁有的寫鎖的過程)源碼示例(來自于《java并發(fā)編程的藝術(shù)》):

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不支持鎖升級,這是因為如果多個線程獲取到了讀鎖涡驮,其中任何一個線程獲取到了寫鎖暗甥,修改了數(shù)據(jù),其他的線程感知不到數(shù)據(jù)的更新捉捅,這樣就無法保證數(shù)據(jù)的可見性撤防。

最后總結(jié)

  • 源碼中,涉及了其他部分棒口,本文做了精簡寄月,比如:cachedHoldCounter,firstReader firstReaderHoldCount等屬性,這些屬性并沒有對理解原理有多少影響无牵,主要是提升性能的作用漾肮,所以本文沒有討論。
  • 讀寫鎖還是依賴于AQS的自定義同步器來實現(xiàn)的茎毁,里面的大部分代碼和之前分析的兩篇文章《Java鎖之ReentrantLock》差不多克懊,AQS的大部分解析已經(jīng)在這兩篇文章已經(jīng)解析過了,如果讀者對此還有疑惑的地方七蜘,可以看看這兩篇文章谭溉。
  • 讀寫鎖的巧妙設(shè)計,就是對AQS的鎖狀態(tài)進行為運算橡卤,區(qū)分了讀狀態(tài)和寫狀態(tài)扮念。
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市碧库,隨后出現(xiàn)的幾起案子柜与,更是在濱河造成了極大的恐慌巧勤,老刑警劉巖,帶你破解...
    沈念sama閱讀 222,590評論 6 517
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件旅挤,死亡現(xiàn)場離奇詭異踢关,居然都是意外死亡,警方通過查閱死者的電腦和手機粘茄,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 95,157評論 3 399
  • 文/潘曉璐 我一進店門签舞,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人柒瓣,你說我怎么就攤上這事儒搭。” “怎么了芙贫?”我有些...
    開封第一講書人閱讀 169,301評論 0 362
  • 文/不壞的土叔 我叫張陵搂鲫,是天一觀的道長。 經(jīng)常有香客問我磺平,道長魂仍,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 60,078評論 1 300
  • 正文 為了忘掉前任拣挪,我火速辦了婚禮擦酌,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘菠劝。我一直安慰自己赊舶,他們只是感情好,可當我...
    茶點故事閱讀 69,082評論 6 398
  • 文/花漫 我一把揭開白布赶诊。 她就那樣靜靜地躺著笼平,像睡著了一般。 火紅的嫁衣襯著肌膚如雪舔痪。 梳的紋絲不亂的頭發(fā)上寓调,一...
    開封第一講書人閱讀 52,682評論 1 312
  • 那天,我揣著相機與錄音锄码,去河邊找鬼捶牢。 笑死,一個胖子當著我的面吹牛巍耗,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播渐排,決...
    沈念sama閱讀 41,155評論 3 422
  • 文/蒼蘭香墨 我猛地睜開眼炬太,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了驯耻?” 一聲冷哼從身側(cè)響起亲族,我...
    開封第一講書人閱讀 40,098評論 0 277
  • 序言:老撾萬榮一對情侶失蹤炒考,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后霎迫,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體斋枢,經(jīng)...
    沈念sama閱讀 46,638評論 1 319
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,701評論 3 342
  • 正文 我和宋清朗相戀三年知给,在試婚紗的時候發(fā)現(xiàn)自己被綠了瓤帚。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,852評論 1 353
  • 序言:一個原本活蹦亂跳的男人離奇死亡涩赢,死狀恐怖戈次,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情筒扒,我是刑警寧澤怯邪,帶...
    沈念sama閱讀 36,520評論 5 351
  • 正文 年R本政府宣布,位于F島的核電站花墩,受9級特大地震影響悬秉,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜冰蘑,卻給世界環(huán)境...
    茶點故事閱讀 42,181評論 3 335
  • 文/蒙蒙 一和泌、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧懂缕,春花似錦允跑、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,674評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至工碾,卻和暖如春弱睦,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背渊额。 一陣腳步聲響...
    開封第一講書人閱讀 33,788評論 1 274
  • 我被黑心中介騙來泰國打工况木, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人旬迹。 一個月前我還...
    沈念sama閱讀 49,279評論 3 379
  • 正文 我出身青樓火惊,卻偏偏與公主長得像,于是被迫代替她去往敵國和親奔垦。 傳聞我的和親對象是個殘疾皇子屹耐,可洞房花燭夜當晚...
    茶點故事閱讀 45,851評論 2 361

推薦閱讀更多精彩內(nèi)容