(轉)ReentrantLock實現原理及源碼分析


背景:ReetrantLock底層是基於AQS實現的(CAS+CHL),有公平和非公平兩種區別。

這種底層機制,很有必要通過跟蹤源碼來進行分析。

參考

ReentrantLock實現原理及源碼分析

源碼分析

接下來我們從源碼角度來看看ReentrantLock的實現原理,它是如何保證可重入性,又是如何實現公平鎖的。

  ReentrantLock是基於AQS的,AQS是Java並發包中眾多同步組件的構建基礎,它通過一個int類型的狀態變量state和一個FIFO隊列來完成共享資源的獲取,線程的排隊等待等。AQS是個底層框架,采用模板方法模式,它定義了通用的較為復雜的邏輯骨架,比如線程的排隊,阻塞,喚醒等,將這些復雜但實質通用的部分抽取出來,這些都是需要構建同步組件的使用者無需關心的,使用者僅需重寫一些簡單的指定的方法即可(其實就是對於共享變量state的一些簡單的獲取釋放的操作)。

  上面簡單介紹了下AQS,詳細內容可參考本人的另一篇文章《Java並發包基石-AQS詳解》,此處就不再贅述了。先來看常用的幾個方法,我們從上往下推。

無參構造器(默認為非公平鎖)

public ReentrantLock() {
        sync = new NonfairSync();//默認是非公平的
    }

sync是ReentrantLock內部實現的一個同步組件,它是Reentrantlock的一個靜態內部類,繼承於AQS,后面我們再分析。

  帶布爾值的構造器(是否公平)

public ReentrantLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();//fair為true,公平鎖;反之,非公平鎖
    }

看到了吧,此處可以指定是否采用公平鎖,FailSync和NonFailSync亦為Reentrantlock的靜態內部類,都繼承於Sync

小結

  其實從上面這寫方法的介紹,我們都能大概梳理出ReentrantLock的處理邏輯,其內部定義了三個重要的靜態內部類,Sync,NonFairSync,FairSync。Sync作為ReentrantLock中公用的同步組件,繼承了AQS(要利用AQS復雜的頂層邏輯嘛,線程排隊,阻塞,喚醒等等);NonFairSync和FairSync則都繼承Sync,調用Sync的公用邏輯,然后再在各自內部完成自己特定的邏輯(公平或非公平)。

 NonFairSync(非公平可重入鎖)

static final class NonfairSync extends Sync {//繼承Sync
        private static final long serialVersionUID = 7316153563782823691L;
        /** 獲取鎖 */
        final void lock() {
            if (compareAndSetState(0, 1))//CAS設置state狀態,若原值是0,將其置為1
                setExclusiveOwnerThread(Thread.currentThread());//將當前線程標記為已持有鎖
            else
                acquire(1);//若設置失敗,調用AQS的acquire方法,acquire又會調用我們下面重寫的tryAcquire方法。這里說的調用失敗有兩種情況:1當前沒有線程獲取到資源,state為0,但是將state由0設置為1的時候,其他線程搶占資源,將state修改了,導致了CAS失敗;2 state原本就不為0,也就是已經有線程獲取到資源了,有可能是別的線程獲取到資源,也有可能是當前線程獲取的,這時線程又重復去獲取,所以去tryAcquire中的nonfairTryAcquire我們應該就能看到可重入的實現邏輯了。
        }
        protected final boolean tryAcquire(int acquires) {
            return nonfairTryAcquire(acquires);//調用Sync中的方法
        }
    }

 

nonfairTryAcquire()

final boolean nonfairTryAcquire(int acquires) {
            final Thread current = Thread.currentThread();//獲取當前線程
            int c = getState();//獲取當前state值
            if (c == 0) {//若state為0,意味着沒有線程獲取到資源,CAS將state設置為1,並將當前線程標記我獲取到排他鎖的線程,返回true
                if (compareAndSetState(0, acquires)) {
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
            else if (current == getExclusiveOwnerThread()) {//若state不為0,但是持有鎖的線程是當前線程
                int nextc = c + acquires;//state累加1
                if (nextc < 0) // int類型溢出了
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);//設置state,此時state大於1,代表着一個線程多次獲鎖,state的值即是線程重入的次數
                return true;//返回true,獲取鎖成功
            }
            return false;//獲取鎖失敗了
        }

 

簡單總結下流程:(ps:獲取鎖的過程,也是共享鎖的實現過程

    1.先獲取state值,若為0,意味着此時沒有線程獲取到資源,CAS將其設置為1,設置成功則代表獲取到排他鎖了;

    2.若state大於0,肯定有線程已經搶占到資源了,此時再去判斷是否就是自己搶占的,是的話,state累加,返回true,重入成功,state的值即是線程重入的次數;

    3.其他情況,則獲取鎖失敗。

  來看看可重入公平鎖的處理邏輯

  FairSync

static final class FairSync extends Sync {
        private static final long serialVersionUID = -3000897897090466540L;

        final void lock() {
            acquire(1);//直接調用AQS的模板方法acquire,acquire會調用下面我們重寫的這個tryAcquire
        }

        protected final boolean tryAcquire(int acquires) {
            final Thread current = Thread.currentThread();//獲取當前線程
            int c = getState();//獲取state值
            if (c == 0) {//若state為0,意味着當前沒有線程獲取到資源,那就可以直接獲取資源了嗎?NO!這不就跟之前的非公平鎖的邏輯一樣了嘛。看下面的邏輯
                if (!hasQueuedPredecessors() &&//判斷在時間順序上,是否有申請鎖排在自己之前的線程,若沒有,才能去獲取,CAS設置state,並標記當前線程為持有排他鎖的線程;反之,不能獲取!這即是公平的處理方式。
                    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;
        }
    }

 可以看到,公平鎖的大致邏輯與非公平鎖是一致的,不同的地方在於有了!hasQueuedPredecessors()這個判斷邏輯,即便state為0,也不能貿然直接去獲取,要先去看有沒有還在排隊的線程,若沒有,才能嘗試去獲取,做后面的處理。反之,返回false,獲取失敗。

  看看這個判斷是否有排隊中線程的邏輯

  hasQueuedPredecessors()

public final boolean hasQueuedPredecessors() {
        Node t = tail; // 尾結點
        Node h = head;//頭結點
        Node s;
        return h != t &&
            ((s = h.next) == null || s.thread != Thread.currentThread());//判斷是否有排在自己之前的線程
    }

 需要注意的是,這個判斷是否有排在自己之前的線程的邏輯稍微有些繞,我們來梳理下,由代碼得知,有兩種情況會返回true,我們將此邏輯分解一下(注意:返回true意味着有其他線程申請鎖比自己早,需要放棄搶占)

  1. h !=t && (s = h.next) == null,這個邏輯成立的一種可能是head指向頭結點,tail此時還為null。考慮這種情況:當其他某個線程去獲取鎖失敗,需構造一個結點加入同步隊列中(假設此時同步隊列為空),在添加的時候,需要先創建一個無意義傀儡頭結點(在AQS的enq方法中,這是個自旋CAS操作),有可能在將head指向此傀儡結點完畢之后,還未將tail指向此結點。很明顯,此線程時間上優於當前線程,所以,返回true,表示有等待中的線程且比自己來的還早。

  2.h != t && (s = h.next) != null && s.thread != Thread.currentThread()同步隊列中已經有若干排隊線程且當前線程不是隊列的老二結點,此種情況會返回true。假如沒有s.thread !=Thread.currentThread()這個判斷的話,會怎么樣呢?若當前線程已經在同步隊列中是老二結點(頭結點此時是個無意義的傀儡結點),此時持有鎖的線程釋放了資源,喚醒老二結點線程,老二結點線程重新tryAcquire(此邏輯在AQS中的acquireQueued方法中),又會調用到hasQueuedPredecessors,不加s.thread !=Thread.currentThread()這個判斷的話,返回值就為true,導致tryAcquire失敗。

ps:一句話就是檢查當前線程前面有沒有等待的線程

  最后,來看看ReentrantLock的tryRelease,定義在Sync中

protected final boolean tryRelease(int releases) {
            int c = getState() - releases;//減去1個資源
            if (Thread.currentThread() != getExclusiveOwnerThread())
                throw new IllegalMonitorStateException();
            boolean free = false;
            //若state值為0,表示當前線程已完全釋放干凈,返回true,上層的AQS會意識到資源已空出。若不為0,則表示線程還占有資源,只不過將此次重入的資源的釋放了而已,返回false。
            if (c == 0) {
                free = true;//
                setExclusiveOwnerThread(null);
            }
            setState(c);
            return free;
        }

 

總結

ReentrantLock是一種可重入的,可實現公平性的互斥鎖,它的設計基於AQS框架,可重入和公平性的實現邏輯都不難理解,每重入一次,state就加1,當然在釋放的時候,也得一層一層釋放。至於公平性,在嘗試獲取鎖的時候多了一個判斷:是否有比自己申請早的線程在同步隊列中等待,若有,去等待;若沒有,才允許去搶占。

 

ReentrantLock原理

ps:這篇博客講起來更加的通俗易懂

AQS使用一個FIFO的隊列表示排隊等待鎖的線程,隊列頭節點稱作“哨兵節點”或者“啞節點”,它不與任何線程關聯。其他的節點與等待線程關聯,每個節點維護一個等待狀態waitStatus

ReentrantLock的基本實現可以概括為:先通過CAS嘗試獲取鎖。如果此時已經有線程占據了鎖,那就加入AQS隊列並且被掛起。當鎖被釋放之后,排在CLH隊列隊首的線程會被喚醒,然后CAS再次嘗試獲取鎖。在這個時候,如果:

非公平鎖:如果同時還有另一個線程進來嘗試獲取,那么有可能會讓這個線程搶先獲取;

公平鎖:如果同時還有另一個線程進來嘗試獲取,當它發現自己不是在隊首的話,就會排到隊尾,由隊首的線程獲取到鎖。(區別)

可重入鎖。可重入鎖是指同一個線程可以多次獲取同一把鎖。ReentrantLock和synchronized都是可重入鎖。

可中斷鎖。可中斷鎖是指線程嘗試獲取鎖的過程中,是否可以響應中斷。synchronized是不可中斷鎖,而ReentrantLock則提供了中斷功能。

公平鎖與非公平鎖。公平鎖是指多個線程同時嘗試獲取同一把鎖時,獲取鎖的順序按照線程達到的順序,而非公平鎖則允許線程“插隊”。synchronized是非公平鎖,而ReentrantLock的默認實現是非公平鎖,但是也可以設置為公平鎖。

lock()

1. 第一步。嘗試去獲取鎖。如果嘗試獲取鎖成功,方法直接返回。

2. 第二步,入隊。由於上文中提到線程A已經占用了鎖,所以B和C執行tryAcquire失敗,並且入等待隊列。如果線程A拿着鎖死死不放,那么B和C就會被掛起。

3. 第三步,掛起。B和C相繼執行acquireQueued(final Node node, int arg)。這個方法讓已經入隊的線程嘗試獲取鎖,若失敗則會被掛起。

線程入隊后能夠掛起的前提是,它的前驅節點的狀態為SIGNAL,它的含義是“Hi,前面的兄弟,如果你獲取鎖並且出隊后,記得把我喚醒!”。所以shouldParkAfterFailedAcquire會先判斷當前節點的前驅是否狀態符合要求,若符合則返回true,然后調用parkAndCheckInterrupt,將自己掛起。如果不符合,再看前驅節點是否>0(CANCELLED),若是那么向前遍歷直到找到第一個符合要求的前驅,若不是則將前驅節點的狀態設置為SIGNAL。

 整個流程中,如果前驅結點的狀態不是SIGNAL,那么自己就不能安心掛起,需要去找個安心的掛起點,同時可以再嘗試下看有沒有機會去嘗試競爭鎖。

    最終隊列可能會如下圖所示

static final class Node {
        /** waitStatus值,表示線程已被取消(等待超時或者被中斷)*/
        static final int CANCELLED =  1;
        /** waitStatus值,表示后繼線程需要被喚醒(unpaking)*/
        static final int SIGNAL    = -1;
        /**waitStatus值,表示結點線程等待在condition上,當被signal后,會從等待隊列轉移到同步到隊列中 */
        /** waitStatus value to indicate thread is waiting on condition */
        static final int CONDITION = -2;
       /** waitStatus值,表示下一次共享式同步狀態會被無條件地傳播下去
        static final int PROPAGATE = -3;
        /** 等待狀態,初始為0 */
        volatile int waitStatus;
        /**當前結點的前驅結點 */
        volatile Node prev;
        /** 當前結點的后繼結點 */
        volatile Node next;
        /** 與當前結點關聯的排隊中的線程 */
        volatile Thread thread;
        /** ...... */
    }

unlock()

如果理解了加鎖的過程,那么解鎖看起來就容易多了。流程大致為先嘗試釋放鎖,若釋放成功,那么查看頭結點的狀態是否為SIGNAL,如果是則喚醒頭結點的下個節點關聯的線程,如果釋放失敗那么返回false表示解鎖失敗。這里我們也發現了,每次都只喚起頭結點的下一個節點關聯的線程。

 用一張流程圖總結一下非公平鎖的獲取鎖的過程。 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM