可重入鎖(ReentrantLock)源碼分析

掃描下方二維碼或者微信搜索公衆號菜鳥飛呀飛,便可關注微信公衆號,閱讀更多Spring源碼分析Java併發編程文章。java

微信公衆號

問題

在閱讀本文以前能夠先思考一下如下兩個問題node

  • 1. ReentrantLock是如何在Java層面(非JVM層面)實現鎖的?
  • 2. 什麼是公平鎖?什麼是非公平鎖?

簡介

  • Lock是JUC包下的一個接口,裏面定義了獲取鎖、釋放鎖等和鎖相關的方法,ReentrantLock是Lock接口的一個具體實現類,它的功能是可重入地獨佔式地獲取鎖。這裏有兩個概念,可重入獨佔式。可重入表示的是同一個線程能屢次獲取到鎖。獨佔式表示的是,同一時刻只能有一個線程獲取到鎖。
  • ReentrantLock實現的鎖又能夠分爲兩類,分別是公平鎖非公平鎖,分別由ReentrantLock類中的兩個內部類FairSyncNonfairSync來實現。FiarSync和NonfairSync均繼承了Sync類,而Sync類又繼承了AbstractQueuedSynchronizer(AQS)類,因此ReentrantLock最終是依靠AQS來實現鎖的。關於AQS的知識能夠參考如下兩篇文章。
  • 隊列同步器(AQS)的設計原理
  • 隊列同步器(AQS)源碼分析
  • ReentrantLock有兩個構造方法,一個無參構造方法,一個有參構造方法。經過無參構造方法建立對象時,建立的是非公平鎖;有參構造方法中,能夠傳入一個boolean類型的變量,若是傳入的是true,那麼建立的是公平鎖,若是傳入的是false,那麼建立的是非公平鎖。
/** * Creates an instance of {@code ReentrantLock}. * This is equivalent to using {@code ReentrantLock(false)}. */
public ReentrantLock() {
    sync = new NonfairSync();
}

/** * Creates an instance of {@code ReentrantLock} with the * given fairness policy. * * @param fair {@code true} if this lock should use a fair ordering policy */
public ReentrantLock(boolean fair) {
    sync = fair ? new FairSync() : new NonfairSync();
}
複製代碼

公平鎖

  • 當向ReentrantLock的構造方法中傳入true時,建立的是公平鎖。公平鎖的lock()unlock()方法實現是由FairSync類中的lock()release()方法實現的(其中release()方法定義在AQS中)。
public static void main(String[] args) {
    ReentrantLock lock = new ReentrantLock(true);
    try{
        lock.lock();
        System.out.println("Hello World");
    }finally {
        lock.unlock();
    }
}
複製代碼

獲取鎖

  • 當調用lock.lock()時,會調用到FairSync.lock()方法,FairSync.lock()方法會調用AQS中的acquire()方法。
static final class FairSync extends Sync {
    final void lock() {
        // 調用acquire()獲取同步狀態
        acquire(1);
    }
}
複製代碼
  • 在AQS的acquire()方法中會先調用子類的tryAcquire()方法,此時因爲咱們建立的是公平鎖,因此會調用FairSync類中的tryAcquire()方法。(關於acquire()方法的詳細介紹,能夠參考:隊列同步器(AQS)源碼分析)。編程

  • tryAcquire()方法的做用就是獲取同步狀態(也就是獲取鎖),若是當前線程成功獲取到鎖,那麼就會將AQS中的同步狀態state加1,而後返回true,若是沒有獲取到鎖,將會返回false。FairSync類上tryAcquire()方法的源碼以下。設計模式

protected final boolean tryAcquire(int acquires) {
        final Thread current = Thread.currentThread();
        // 獲取同步狀態state的值(在AQS中,state就至關於鎖,若是線程能成功修改state的值,那麼就表示該線程獲取到了鎖)
        int c = getState();
        if (c == 0) {
            // 若是c等於0,表示尚未任何線程獲取到鎖

            /** * 此時可能存在多個線程同時執行到這兒,均知足c==0這個條件。 * 在if條件中,會先調用hasQueuedPredecessors()方法來判斷隊列中是否已經有線程在排隊,該方法返回true表示有線程在排隊,返回false表示沒有線程在排隊 * 第1種狀況:hasQueuedPredecessors()返回true,表示有線程排隊, * 此時 !hasQueuedPredecessors() == false,因爲&& 運算符的短路做用,if的條件判斷爲false,那麼就不會進入到if語句中,tryAcquire()方法就會返回false * * 第2種狀況:hasQueuedPredecessors()返回false,表示沒有線程排隊 * 此時 !hasQueuedPredecessors() == true, 那麼就會進行&&後面的判斷,就會調用compareAndSetState()方法去進行修改state字段的值 * compareAndSetState()方法是一個CAS方法,它會對state字段進行修改,它的返回值結果又須要分兩種狀況 * 第 i 種狀況:對state字段進行CAS修改爲功,就會返回true,此時if的條件判斷就爲true了,就會進入到if語句中,同時也表示當前線程獲取到了鎖。那麼最終tryAcquire()方法會返回true * 第 ii 種狀況:若是對state字段進行CAS修改失敗,說明在這一瞬間,已經有其餘線程獲取到了鎖,那麼if的條件判斷就爲false了,就不會進入到if語句塊中,最終tryAcquire()方法會返回false。 */
            if (!hasQueuedPredecessors() &&
                compareAndSetState(0, acquires)) {
                // 當前線程成功修改了state字段的值,那麼就表示當前線程獲取到了鎖,那麼就將AQS中鎖的擁有者設置爲當前線程,而後返回true。
                setExclusiveOwnerThread(current);
                return true;
            }
        }
        // 若是c等於0,則表示已經有線程獲取到了鎖,那麼這個時候,就須要判斷獲取到鎖的線程是否是當前線程
        else if (current == getExclusiveOwnerThread()) {
            // 若是是當前線程,那麼就將state的值加1,這就是鎖的重入
            int nextc = c + acquires;
            if (nextc < 0)
                throw new Error("Maximum lock count exceeded");
            // 由於此時確定只有一個線程獲取到了鎖,只有獲取到鎖的線程纔會執行到這行代碼,因此能夠直接調用setState(nextc)方法來修改state的值,這兒不會存在線程安全的問題。
            setState(nextc);
            // 而後返回true,表示當前線程獲取到了鎖
            return true;
        }
        // 若是state不等於0,且當前線程也等於已經獲取到鎖的線程,那麼就返回false,表示當前線程沒有獲取到鎖
        return false;
    }
}
複製代碼
  • 在tryAcquire()方法中,先判斷了同步狀態state是否是等於0。
  • 1. 若是等於0,就表示目前尚未線程持有到鎖,那麼這個時候就會先調用hasQueuedPredecessors()方法判斷同步隊列中有沒有等待獲取鎖的線程,若是有線程在排隊,那麼當前線程確定獲取鎖失敗(由於AQS的設計的原則是FIFO,既然前面有人已經在排隊了,你就不能插隊,老老實實去後面排隊去),那麼tryAcquire()方法會返回false。若是同步隊列中沒有線程排隊,那麼就讓當前線程對state進行CAS操做,若是設置成功,就表示當前獲取到鎖了,返回true;若是CAS失敗,表示在這一瞬間,鎖被其餘線程搶走了,那麼當前線程就獲取鎖失敗,就返回false。
  • 2. 若是不等於0,就表示已經有線程獲取到鎖了,那麼此時就會去判斷當前線程是否是等於已經持有鎖的線程(getExclusiveOwnerThread()方法的做用就是返回已經持有鎖的線程),若是相等,就讓state加1,這就是所謂的重入鎖,重入鎖就是容許同一個線程屢次獲取到鎖。
  • 3. state既不等於0,當前線程也不等於持有鎖的線程,那麼就返回false,表示當前線程獲取到鎖失敗。
  • 就這樣,經過FairSync類的tryAcquire()方法,就實現了公平鎖獲取鎖的邏輯。在不一樣的鎖的實現中,tryAcquire()方法的邏輯是不同的,例如在非公平鎖中,NonfairSync類的tryAcquire()中,代碼邏輯和FairSync類的tryAcquire()方法就不同,在非公平鎖的實現中,當線程嘗試對state進行CAS操做以前,沒有對同步隊列中有沒有線程在排隊進行判斷(即沒有調用hasQueuedPredecessors()方法)。
  • hasQueuedPredecessors()方法的做用是判斷同步隊列中有沒有線程在排隊,若是有,就返回true,若是沒有,就返回false。其源碼以下,在貼出的代碼片斷中,對該方法進行了詳細的解釋(要看懂該方法,須要對AQS的設計原理有必定的瞭解,能夠閱讀這一篇文章:隊列同步器(AQS)的設計原理)。
public final boolean hasQueuedPredecessors() {
    Node t = tail; // Read fields in reverse initialization order
    Node h = head;
    Node s;

    /** * 該方法的做用是判斷隊列中是否有線程在排隊,返回true表示有線程排隊,返回false表示沒有線程排隊 * * 首先判斷 h!=t,即判斷頭結點和尾結點是否相等,頭結點和尾結點相等只有兩種狀況, * 第一:隊列尚未被初始化,此時head = null, tail = null,此時線程不須要排隊。 * 第二:已經有一個線程獲取到了鎖,當第二個線程進來獲取鎖時,由於獲取不到鎖,因此會被須要入隊,入隊以前它須要先初始化隊列, * 在初始化時,在enq()方法中,第一步是先將tail = head,而後第二步再將當前線程表明的Node設置爲tail。因此在這兩步操做 * 的中間的一瞬間,是存在tail = head這個可能性的。此時對全部線程而言是也不須要排隊 * 當 h!=t 的結果爲true時,接下來會判斷((s = h.next) == null || s.thread != Thread.currentThread()),先判斷(s = h.next) == null * * 判斷(s = h.next) == null 時,先令 s = h.next ,表示獲取到頭結點的的下一個節點,即第二個節點,若是 s == null 則表示第二個節點爲null,因爲前面已經判斷了h!=t, * 說明此時已經有第二個線程進入到了隊列中,只不過它還沒來及將head節點的next指針的指向修改,因此此時線程線程須要排隊, * 由於||運算的短路緣由,當(s = h.next) == null 的結果爲true時,就不會進入到後面的判斷了,而此時hasQueuedPredecessors()會返回true,表示線程須要排隊。 * 當s不爲null時,會進行s.thread != Thread.currentThread() 的判斷 * 它會判斷s節點中的線程是否不等於當前線程,若是不等於當前線程,hasQueuedPredecessors()會返回true,說明當前線程須要排隊。 * 由於當第二個節點不是當前線程,那麼就說明當前線程應該至少是排在隊列中的第三位,那麼它須要排隊。 * 若是s節點中的線程等於當前線程,那麼說明當前線程是排在隊列中的第二位(第一位是已經獲取到鎖的線程),此時線程是不須要排隊的, * 由於可能在這一瞬間已經獲取到鎖的線程釋放了鎖,那麼排在隊列中第二位的線程還排啥子隊哦,直接去嘗試獲取鎖便可。 * * * */

    return h != t &&
        ((s = h.next) == null || s.thread != Thread.currentThread());
}
複製代碼

釋放鎖

  • 當調用lock.unlock()方法釋放鎖時,會調用到AQS的release()方法,若是釋放鎖成功,就會將AQS中的state變量的值減爲0。在release()方法中,會先調用到tryRelease()方法,而後還會調用unparkSuccessor()方法來喚醒同步隊列中在等待獲取鎖的一個線程。tryRelease()方法的邏輯是由子類實現的,不一樣鎖有不一樣的實現。可是對於ReentrantLock而言,公平鎖和非公平鎖的釋放鎖的邏輯是同樣的,均是調用Sync類的tryRelease()方法。
  • AQS的release()方法源碼以下。
public final boolean release(int arg) {
    if (tryRelease(arg)) {
        // 當釋放鎖成功之後,須要喚醒同步隊列中的其餘線程
        Node h = head;
        // 當waitStatus!=0時,表示同步隊列中還有其餘線程在等待獲取鎖
        if (h != null && h.waitStatus != 0)
            // 喚醒同步隊列中下一個能嘗試獲取鎖的線程
            unparkSuccessor(h);
        return true;
    }
    return false;
}
複製代碼
  • Sync類的tryRelease()方法的源碼和代碼註釋以下。
protected final boolean tryRelease(int releases) {
    int c = getState() - releases;
    // 判斷當前線程是否是持有鎖的線程,若是不是就拋出異常
    if (Thread.currentThread() != getExclusiveOwnerThread())
        throw new IllegalMonitorStateException();
    // 由於上面一步已經確認了是當前線程持有鎖,因此在修改state時,確定是線程安全的
    boolean free = false;
    // 由於可能鎖被重入了,重入了幾回就須要釋放幾回鎖,因此這個地方須要判斷,只有當state=0時才表示徹底釋放了鎖。
    if (c == 0) {
        free = true;
        setExclusiveOwnerThread(null);
    }
    setState(c);
    return free;
}
複製代碼
  • tryRelease()方法會返回有沒有釋放鎖成功的標識,當表示同步狀態的變量state被減爲0了,tryRelease()方法纔會返回true,不然返回false,若是鎖被重入了屢次,就須要屢次調用tryRelease()方法。當tryRelease()執行完之後,會回到AQS中的release()方法中,若是tryRelease()方法返回true,表示鎖被釋放了,這個時候若是同步隊列中有線程在排隊,那麼就去調用unparkSuccessor()方法去喚醒同步隊列中下一個離頭結點最近的且有資格獲取鎖的線程。爲何是離頭結點最近呢?由於AQS要保證FIFO。爲何是有資格呢?由於同步隊列中有的線程的狀態是被取消的,即Node節點的waitStatus=1,此時這種線程是不能被喚醒去搶鎖的。另外,這裏只是去喚醒線程去嘗試獲取鎖,不保證能獲取到。喚醒線程使用的方法是LockSupport.unpark()
  • unparkSuccessor()方法的源代碼和註釋以下。
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;
    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;
        /** * 同步隊列中的節點鎖表明的線程,可能被取消了,此時這個節點的waitStatus=1 * 所以這個時候利用for循環從同步隊列尾部開始向前遍歷,判斷節點是否是被取消了 * 正常狀況下,當頭結點釋放鎖之後,應該喚醒同步隊列中的第二個節點,可是若是第二個節點的線程被取消了,此時就不能喚醒它了, * 就應該判斷第三個節點,若是第三個節點也被取消了,再依次日後判斷,直到第一次出現沒有被取消的節點。若是都被取消了,此時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);
}
複製代碼

非公平鎖

  • 當使用ReentrantLock的無參構造器或者有參構造器中傳入false時,建立的是非公平鎖。非公平鎖的具體實現是由NonfairSync類來實現的,非公平鎖的獲取邏輯與公平鎖的獲取邏輯存在一點差別,鎖的釋放二者徹底同樣。

獲取鎖

  • 非公平鎖的獲取會先調用NonfairSync類的lock()方法。非公平鎖不會去判斷同步隊列中有沒有人排隊,而是先直接去嘗試修改state變量的值爲1,若是修改爲功,就表示線程獲取到了鎖,而後lock()方法結束。若是修改失敗,就再去調用AQS的acquire()方法去嘗試獲取鎖。
static final class NonfairSync extends Sync {
    
    final void lock() {
        // 先嚐試獲取鎖,若是獲取鎖失敗,就再去調用AQS的acquire()方法
        if (compareAndSetState(0, 1))
            setExclusiveOwnerThread(Thread.currentThread());
        else
            acquire(1);
    }
}
複製代碼
  • 當調用到AQS的acquire()方法時,在acquire()方法中會調用AQS子類的tryAcquire()方法,此時調用的是NonfairSync類的tryAcquire()方法。源碼以下。
protected final boolean tryAcquire(int acquires) {
    // 調用nonfairTryAcquire()方法
    return nonfairTryAcquire(acquires);
}
複製代碼
  • NonfairSync的tryAcquire()方法會調用nonfairTryAcquire()方法。
final boolean nonfairTryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();
    if (c == 0) {
        // 與公平鎖的不一樣之處在於,公平在在進行CAS操做以前,會先判斷同步隊列中是否有人排隊。
        // !hasQueuedPredecessors()
        if (compareAndSetState(0, acquires)) {
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    else if (current == getExclusiveOwnerThread()) {
        int nextc = c + acquires;
        if (nextc < 0) // overflow
            throw new Error("Maximum lock count exceeded");
        setState(nextc);
        return true;
    }
    return false;
}
複製代碼
  • 能夠看到nonfairTryAcquire()方法的邏輯與FairSync的tryAcquire()方法的邏輯很是類似,惟一的區別就是當c == 0時,非公平鎖沒有去判斷同步隊列中是否有人在排隊,而是直接調用CAS方法去設置state的值。而公平鎖則是先判斷同步隊列中是否有人排隊,若是沒有人排隊,才調用CAS方法去設置state的值。即非公平鎖沒有調用hasQueuedPredecessors()方法。其餘的邏輯,公平鎖與非公平鎖的邏輯就同樣了。

釋放鎖

  • 非公平鎖的釋放邏輯與公平鎖的釋放邏輯同樣,最終都是調用Sync類的tryRelease()方法。

總結

  • 本文主要介紹瞭如何經過ReentrantLock來建立公平鎖和非公平鎖,經過默認的無參構造方法建立的是非公平鎖;當使用有參構造器建立時,若是參數傳入的是false,則建立的是非公平鎖,若是傳入的是true,則建立的是公平鎖。
  • 實現公平鎖的同步組件的類是FairSync,實現非公平鎖的同步組件的類是NonfairSync,它們都是隊列同步器AQS的子類。
  • 經過分別分析FairSync和NonfairSync類的tryAcquire()方法,分析了公平鎖和非公平鎖的獲取鎖的流程。同時對比二者的實現代碼,發現非公平鎖在獲取鎖時不會判斷同步隊列中有沒有線程在排隊,而是直接嘗試去修改state的值,而公平鎖在獲取鎖時,會先判斷同步隊列中有沒有線程在排隊,若是沒有才會嘗試去修改state的值。
  • 公平鎖和非公鎖在釋放鎖時的邏輯是同樣的。
  • 最後回答一下文章開頭的兩個問題。
  • 1. ReentrantLock是如何在Java層面(非JVM實現)實現鎖的?
  • ReentrantLock類經過組合一個同步組件Sync來實現鎖,這個同步組件繼承了AQS,並重寫了AQS中的tryAcquire()、tryRelease()等方法,最終實際上仍是經過AQS以及重寫的tryAcquire()、tryRelease()等方法來實現鎖的邏輯。ReentrantLock實現鎖的方式,也是JUC包下其餘類型的鎖的實現方法,經過組合一個自定義的同步組件,這個同步組件須要繼承AQS,而後重寫AQS中的部分方法便可實現一把自定義的鎖,一般這個同步組件被定義成內部類。
  • 2. 什麼是公平鎖?什麼是非公平鎖?
  • FairSync同步組件實現的鎖是公平鎖,它獲取鎖的原則是,在同步隊列中等待時間最長的線程獲取鎖,所以稱它爲公平鎖。由NonfairSync同步組件實現的鎖是非公平鎖,它獲取鎖的原則是,同步隊列外的線程在嘗試獲取鎖時,不會判斷隊列中有沒有線程在排隊,而是上來就搶,搶到鎖了就走,搶不到了纔去排隊,所以稱它爲不公平的。

相關推薦

微信公衆號
相關文章
相關標籤/搜索