ReentrantLock實現原理及源碼分析

  ReentrantLock是Java併發包中提供的一個可重入的互斥鎖ReentrantLocksynchronized在基本用法,行爲語義上都是相似的,一樣都具備可重入性。只不過相比原生的Synchronized,ReentrantLock增長了一些高級的擴展功能,好比它能夠實現公平鎖,同時也能夠綁定多個Conditonhtml

可重入性/公平鎖/非公平鎖

  可重入性併發

    所謂的可重入性,就是能夠支持一個線程對鎖的重複獲取,原生的synchronized就具備可重入性,一個用synchronized修飾的遞歸方法,當線程在執行期間,它是能夠反覆獲取到鎖的,而不會出現本身把本身鎖死的狀況。ReentrantLock也是如此,在調用lock()方法時,已經獲取到鎖的線程,可以再次調用lock()方法獲取鎖而不被阻塞。那麼有可重入鎖,就有不可重入鎖,咱們在以前文章中自定義的一個Mutex鎖就是個不可重入鎖,不過使用場景極少而已。框架

  公平鎖/非公平鎖函數

    所謂公平鎖,顧名思義,意指鎖的獲取策略相對公平,當多個線程在獲取同一個鎖時,必須按照鎖的申請時間來依次得到鎖,排排隊,不能插隊;非公平鎖則不一樣,當鎖被釋放時,等待中的線程均有機會得到鎖。synchronized是非公平鎖,ReentrantLock默認也是非公平的,可是能夠經過帶boolean參數的構造方法指定使用公平鎖,但非公平鎖的性能通常要優於公平鎖。源碼分析

  synchronized是Java原生的互斥同步鎖,使用方便,對於synchronized修飾的方法或同步塊,無需再顯式釋放鎖。synchronized底層是經過monitorenter和monitorexit兩個字節碼指令來實現加鎖解鎖操做的。而ReentrantLock作爲API層面的互斥鎖,須要顯式地去加鎖解鎖。 性能

class X { private final ReentrantLock lock = new ReentrantLock(); // ...
 
    public void m() { lock.lock(); // 加鎖
      try { // ... 函數主題
      } finally { lock.unlock() //解鎖
 } } }

源碼分析

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

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

  上面簡單介紹了下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

  再來看看幾個咱們經常使用到的方法

  lock()

public void lock() { sync.lock();//代理到Sync的lock方法上
    }

  Sync的lock方法是抽象的,實際的lock會代理到FairSync或是NonFairSync上(根據用戶的選擇來決定,公平鎖仍是非公平鎖)

  lockInterruptibly()

public void lockInterruptibly() throws InterruptedException {
        sync.acquireInterruptibly(1);//代理到sync的相應方法上,同lock方法的區別是此方法響應中斷
    }

  此方法響應中斷,當線程在阻塞中的時候,若被中斷,會拋出InterruptedException異常 

  tryLock()

public boolean tryLock() { return sync.nonfairTryAcquire(1);//代理到sync的相應方法上
    }

  tryLock,嘗試獲取鎖,成功則直接返回true,不成功也不耽擱時間,當即返回false。

  unlock()

public void unlock() { sync.release(1);//釋放鎖
    }

  釋放鎖,調用sync的release方法,實際上是AQS的release邏輯。

   newCondition()

    獲取一個conditon,ReentrantLock支持多個Condition

public Condition newCondition() { return sync.newCondition(); }

  其餘方法就再也不贅述了,若想繼續瞭解可去API中查看。

  小結

  其實從上面這寫方法的介紹,咱們都能大概梳理出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;//獲取鎖失敗了
        }

  簡單總結下流程:

    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失敗。

  最後,來看看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,固然在釋放的時候,也得一層一層釋放。至於公平性,在嘗試獲取鎖的時候多了一個判斷:是否有比本身申請早的線程在同步隊列中等待,如有,去等待;若沒有,才容許去搶佔。

相關文章
相關標籤/搜索