天天看點

ReentrantLock的lock-unlock流程詳解

最近一段時間在研究jdk裡的concurrent包,分為了線程管理,鎖操作以及原子操作三個部分。線程管理平時用得還算多,但是鎖操作和原子操作基本就沒用過,隻是以前在大學的時候跑了幾個例子玩玩。當看到reentrantlock的時候,發現用法倒是和synchronized有點類似也很簡單,但是内部原理比較複雜。網上查了關于reentrantlock的相關内容,沒發現有誰把它分析得很透徹,隻是有幾篇講了内部的鎖實作機制,隻可惜都是以文字為主,難以把整個内部流程串起來了解,相信很多人沒有真正把這個新的同步機制搞明白,當然也包括我在内。是以花了幾天時間反複調試,終于把最基礎的lock-unlock以及與reentrantlock綁定的condition的await-signal機制以流程圖的形式畫了出來,同時把我認為較為難了解的代碼也分析了其内部原理加以注釋,以此分享給大家,希望能幫助各位更快的了解它的實作原理,也歡迎批評指正。

       目前隻是研究了正常狀态下的同步原理,如果存線上程中斷,其流程将更為複雜。是以暫時先上兩篇無中斷的流程講解,待我能把帶中斷的流程整理清楚了再發出來,以免誤導大家。

       在jdk1.5之前,多線程之間的同步是依靠synchronized來實作。synchronized是java的關鍵字,直接由jvm解釋成為指令進行線程同步管理。因為操作簡單,而且現在jdk的後續版本已經對synchronized進行了很多的優化,是以一直是大家編寫多線程程式常用的同步工具。那為什麼要推出新的同步api呢?jdk1.5釋出的時候,synchronized性能并不好,這可能是concurrent包出現的一個潛在原因,但是更重要的是新的api提供了更靈活,更細粒度的同步操作,以滿足不同的需求。但是問題也很明顯,越靈活可控度越高就越容易出錯,是以大多數人很少使用concurrent裡的同步鎖api。本文并不比較兩者的優劣,畢竟存在即合理。能把更進階的工具用好,有時候提高工作效率,加快異常排查也是很不錯的,不過本人是出于學習其原理及思想才進行研究,工作中用哪一個同樣也會謹慎抉擇。

reentrantlock類圖:

ReentrantLock的lock-unlock流程詳解

abstractownablesynchronizer類保持和擷取獨占線程。

abstractqueuedsynchronizer是以虛拟隊列的方式管理線程的鎖擷取與鎖釋放,以及各種情況下的線程中斷。提供了預設的同步實作,但是擷取鎖和釋放鎖的實作定義為抽象方法,由子類實作。目的是使開發人員可以自由定義擷取鎖以及釋放鎖的方式。

sync是reentrantlock的内部抽象類,實作了簡單的擷取鎖和釋放鎖。

nonfairsync和fairsync分别表示“非公平鎖”和“公平鎖”,都繼承于sync,并且都是reentrantlock的内部類。

reentrantlock實作了lock接口的lock-unlock方法,根據fair參數決定使用nonfairsync還是fairsync。

這裡面有兩個重點内容:

abstractqueuedsynchronizer内部的node

abstractqueuedsynchronizer内部的state

node:

ReentrantLock的lock-unlock流程詳解

       node是對每一個通路同步代碼的線程的封裝。不僅包括了需要同步的線程,而且也包含了每個線程的狀态,比如等待解除阻塞,等待條件喚醒,已經被取消等等。同時node還關聯了前驅和後繼,即prev和next。個人認為是為了以集中的方式管理多個不同狀态的線程,當不同的線程發生狀态改變時,可以盡快的反應到别的線程上,提高運作效率。比如某個node的prev已經被取消了,那麼當對這個prev解除阻塞的時候就可以被忽略掉,進而嘗試解除該node的阻塞狀态。

       多個node連接配接起來成為了虛拟隊列(因為不存在真正的隊列容器将每個元素裝起來是以說是虛拟的,我把它稱為release隊列,意思是等待釋放),那麼就得有head和tail。針對公平鎖,head是不帶線程的特殊node,隻有next,而最新一個請求鎖的線程取鎖失敗時就把它添加到隊尾,即tail。但是對于非公平鎖,新請求鎖的線程會插隊,也許會插到最前面,也許不會。

       這裡可能有人會有疑問:head放在隊列中有什麼用處?為什麼不是一個等待鎖的線程作為head呢?原因很簡單,因為每個等待線程都有可能被中斷而取消,對于一個已經取消的線程,自然是有機會就把它gc了。那麼gc前一定得讓後續的node成為head,這樣一來sethead的操作過于分散,而且要應對多種線程狀态的變化來設定head,這樣就太麻煩了。是以這裡很巧妙地将head的next設定為等待鎖的node,head就相當于一個引導的作用,因為head沒有線程,是以不存在“取消”這種狀态。

state:

ReentrantLock的lock-unlock流程詳解

state是用來記錄鎖的持有情況。

沒有線程持有鎖的時候,state為0。

當某個線程擷取鎖時,state的值增加,具體增加多少開發人員可自定義,預設為1,表示該鎖正在被一個線程占有。

當某個已經占用鎖的線程再次擷取到鎖時,state再增長,此為重入鎖。

當占有鎖的線程釋放鎖時,state也要減去當初占有時傳入的值,預設為1。

多個線程競争鎖的時候,state必須通過cas進行設定,這樣才能保證鎖隻能有一個線程持有。當然這是排它鎖的規則,共享鎖就不是這樣了。

公平鎖和非公平鎖的不同僅在于修改state的時機。看下面的代碼就能明白:

ReentrantLock的lock-unlock流程詳解

// reentrantlock.class  

protected final boolean tryacquire(int acquires) {  

    final thread current = thread.currentthread();  

    int c = getstate();  

    if (c == 0) {  

        // 此為公平鎖的實作,而非公平鎖不調用hasqueuedpredecessors方法,即不需要判斷隊列裡是否有内容,直接通過cas修改state來競争鎖  

        if (!hasqueuedpredecessors() &&  

            compareandsetstate(0, acquires)) {  

            setexclusiveownerthread(current);  

            return true;  

        }  

    }  

    else if (current == getexclusiveownerthread()) {  

        int nextc = c + acquires;  

        if (nextc < 0)  

            throw new error("maximum lock count exceeded");  

        setstate(nextc);  

        return true;  

    return false;  

}  

       基本上把node和state的意義弄明白了,一個正常的lock-unlock過程應該很容易明白。但是代碼裡有很多地方要針對已取消的線程做特殊處理,是以了解上還是會有困難。因為已經有幾篇文章進行了源碼講解,是以這裡我就不再把每段源代碼都拿出來細講了。

先看看lock()的流程:(圖中的node0和node1在源代碼中不存在,是我為了友善說明清楚才添加的别稱)

ReentrantLock的lock-unlock流程詳解

如圖所示,彩色的字都是一個cas操作,其中三個紅色cas都對成功和失敗有相應地處理,為什麼另外一個藍色cas不關心設定是否成功呢?下面這段代碼裡我給出了解釋:

ReentrantLock的lock-unlock流程詳解

// abstractqueuedsynchronizer.class  

private static boolean shouldparkafterfailedacquire(node pred, node node) {  

    int ws = pred.waitstatus;  

    if (ws == node.signal)  

        /* 

         * this node has already set status asking a release 

         * to signal it, so it can safely park. 

         */  

    if (ws > 0) {  

         * predecessor was cancelled. skip over predecessors and 

         * indicate retry. 

        do {  

            node.prev = pred = pred.prev;  

        } while (pred.waitstatus > 0);  

        pred.next = node;  

    } else {  

         * waitstatus must be 0 or propagate.  indicate that we 

         * need a signal, but don't park yet.  caller will need to 

         * retry to make sure it cannot acquire before parking. 

         * 為什麼不關心是否成功卻還要設定呢? 

         * 

         * 如果設定失敗,表示前驅已經被signal了。如果前驅是head,說明有機會擷取鎖,是以傳回false後還可以再次tryacquire 

         * 如果設定成功,表示前驅等待signal。如果再次确認pred.waitstatus仍然是node.signal,則表明前驅等待釋放鎖的情況下必須阻塞目前線程 

         * 是以傳回true後即被park 

        compareandsetwaitstatus(pred, ws, node.signal);  

再看下unlock()的流程圖:

ReentrantLock的lock-unlock流程詳解

如圖所示,這裡的粉紅色折線與lock流程圖裡的粉紅色虛折線對應,即線程a調用lock阻塞與線程b調用unlock解除線程a的阻塞。同時可以看到unlock隻有一個cas操作,但是也不用關心設定是否成功。我給這段代碼做了下面解釋:

ReentrantLock的lock-unlock流程詳解

private void unparksuccessor(node node) {  

    /* 

     * if status is negative (i.e., possibly needing signal) try 

     * to clear in anticipation of signalling.  it is ok if this 

     * fails or if status is changed by waiting thread. 

     */  

    int ws = node.waitstatus;  

     * 為什麼不關心是否成功卻還要設定呢? 

     * 

     * 注意這裡的node實際就是head 

     *  

     * 如果設定成功,即head.waitstatus=0,則可以讓這時即将被阻塞的線程有機會再次調用tryacquire擷取鎖。 

     * 也就是讓shouldparkafterfailedacquire方法裡的compareandsetwaitstatus(pred, ws, node.signal)執行失敗傳回false,這樣就能再有機會再tryacquire了 

     * 如果設定失敗,新跟随在head後面的線程被阻塞,但是沒關系,下面的代碼會立即将這個阻塞線程釋放掉 

    if (ws < 0)  

        compareandsetwaitstatus(node, ws, 0);  

     * thread to unpark is held in successor, which is normally 

     * just the next node.  but if cancelled or apparently null, 

     * traverse backwards from tail to find the actual 

     * non-cancelled successor. 

    node s = node.next;  

    if (s == null || s.waitstatus > 0) {  

        s = null;  

        for (node t = tail; t != null && t != node; t = t.prev)  

            if (t.waitstatus <= 0)  

                s = t;  

    if (s != null)  

        locksupport.unpark(s.thread);  

       以上的兩段代碼說明是我覺得比較難了解的,雖然有英文注釋,但是卻沒說明為什麼這麼做,我也是反複調試才想明白。但是不得不說這兩段代碼的巧妙,盡可能利用cas操作減少阻塞的機會,讓線程能有更多機會擷取鎖,畢竟阻塞線程是核心操作,開銷不小。

本篇隻講了普通的lock-unlock,下篇會講講等待-通知,即condition的await-signal的詳細流程

參考資料: