一步一腳印踏進ReentrantLock源碼解析,衝鴨~~

一、引言

話不多說,扶我起來,我還可以繼續擼。

在學習ReentrantLock源碼之前,先來回顧一下鏈表、隊列數據結構的基本概念~~

二、數據結構

2.1 鏈表(Linked List) 

小學一、二年級的時候,學校組織戶外活動,老師們一般都要求同學之間小手牽着小手。這個場景就很類似一個單鏈表。每個小朋友可以看作一個節點信息,然後通過牽手的方式,形成整個鏈表結構。

1、鏈表是以節點的形式來存儲數據,可以稱之爲:鏈式存儲

2、每個節點都包含所需要存放對應的數據(data 域),以及指向下一個節點的元素(next 域)。

3、鏈表可以帶頭節點也可以不帶頭節點,根據實際需求來確定,頭節點一般不會存放具體數據,只會指向下一個節點。

4、鏈表總的來說可以分之爲幾種類型:單鏈表、雙向鏈表、環形鏈表(循環鏈表)

單鏈表(帶頭節點) 結構示意圖:

單鏈表總的來看,理解比較簡單,但是缺點也是顯而易見的,查找的方向只能是一個方向,並且在某些操作下,單鏈表會比較費勁。比如說在刪除某個單鏈表節點時,我們需要找到刪除節點的,前一個節點才能夠進行刪除。

這個時候,就有了我們雙向鏈表:

雙向鏈表(帶頭節點) 結構示意圖:

相對應單鏈表來說,雙向鏈表多了一個pre屬性,這個屬性會指向當前節點的上一個節點,所以稱之爲雙向鏈表。

換句話來說雙向鏈表就是你中有我,我中有你哈哈哈哈~~~~

環形鏈表(循環鏈表)結構示意圖:

環形鏈表也就是,鏈表最後一個節點,指向了頭節點,整體構成一個環形。其實理解了單鏈表結構,後面兩種結構都比較好理解。

2.2 隊列(Queue)

隊列其實只要記住最重要特點:遵循先入先出的原則,先存入的數據,先取出,後存儲的數據後取出。

在換到生活場景來說,最簡單的就是排隊,最先排隊的人,弄完事最先走了,也就是出隊列了。

隊列也是線性表的一種,它只允許在表的前端進行進行刪除操作,在表的後端進行插入操作。進行刪除操作端叫做隊頭,進行插入的一端叫做隊尾。

 

這裏小編多的就不講了,相信作爲一名碼農來說,這兩種都是很基本、很基本、很基本的數據結構了。

三、AQS隊列同步器

3.1 基本介紹

AQS是什麼呢? 全稱是AbstractQueuedSynchronizer,中文就是隊列同步器,簡單暴力來說,它對應我們Java中的一個抽象類,AQS是ReentrantLock很重要的實現部分。

首先我們需要了解到,在AQS中包含了哪些重要內容,小編這裏給列舉部分出來了。

這裏代碼小編省略很多了,展示了我們所需要關心的內容。(省的擔心你們說小編亂畫圖)

// @author Doug Lea
public abstract class AbstractQueuedSynchronizer
    extends AbstractOwnableSynchronizer
    implements java.io.Serializable {

   static final class Node {
      // 指向上一個節點
      volatile Node prev;
      // 指向下一個節點
      volatile Node next;
      // 存放具體的數據
      volatile Thread thread;
      // 線程的等待狀態
      volatile int waitStatus;
   }

    // 頭節點
    private transient volatile Node head;

    // 尾節點
    private transient volatile Node tail;

    // 鎖狀態
    private volatile int state;


}

3.2 AQS在ReentrantLock中起什麼作用呢?

假設現在要求小夥伴們自己實現一把鎖,你們會怎麼去設計一把鎖呢?

最容易想到的方案就是,首先肯定要有個鎖狀態(假設就是個int 變量 0 自由狀態、1 被鎖狀態),如果一個線程獲取到了鎖,就把這個鎖狀態改成 1,線程釋放鎖就改成0。 那又假設現在我們線程一獲取到了鎖,線程二來了怎麼辦? 線程二又要去哪裏等着呢?  這個時候AQS就給你提供了一系列基本的操作,讓開發者更加專注鎖的實現。

AQS這種設計屬於模板方法模式(行爲型設計模式),使用者需要繼承這個AQS並重寫指定的方法,最後調用AQS提供的模板方法,而這些模板方法會調用使用者重寫的方法。

這麼說把,AQS是用來構建鎖的基礎框架,主要的使用方式是繼承,子類通過繼承AQS並實現它的一系列方法來管理同步狀態。還有我們實現一把鎖肯定避免不了對鎖狀態的更改,AQS還提供了以下三個方法:

getState(): 獲取當前鎖狀態

setState(int newState): 設置當前鎖狀態

compareAndSetState(int expect, int update):CAS設置鎖狀態,CAS能夠保證原子性操作,小編上一篇文章講sync有具體講到。

看到這裏,希望小夥伴能夠對AQS這個抽象類有個大概的認識。

四、ReentrantLock 加鎖過程源碼分析

本文主要注重ReentrantLock 加鎖、解鎖過程源碼分析!!!

本文都是以公平鎖爲主,如果弄懂了公平鎖的過程,再回頭過看看非公平鎖,就很輕鬆了,這個就交給小夥伴你們自己了~

4.1 ReentrantLock結構圖

整體看下ReentrantLock結構:先來個IDEA裏面展示的結構圖,然後小編再結合畫一個更簡單明瞭的結構圖。

4.2 ReentrantLock 重入鎖

重入鎖簡單來說一個線程可以重複獲取鎖資源,雖然ReentrantLock不像synchronized關鍵字一樣支持隱式的重入鎖,但是在調用lock方法時,它會判斷當前嘗試獲取鎖的線程,是否等於已經擁有鎖的線程,如果成立則不會被阻塞(下面講源碼的時候會講到)。

還有ReentrantLock在創建的時候,可以通構造方法指定創建公平鎖還是非公平鎖。這裏是個細節部分,如果知道有公平鎖和非公平鎖,但是不知道怎麼創建,這樣還敢說看過源碼?

    // ReentrantLock 構造方法
    // 默認非公平鎖
    public ReentrantLock() {
        sync = new NonfairSync();
    }

    // 傳入true,創建公平鎖
    public ReentrantLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
    }

怎麼理解公平鎖和非公平鎖呢? 先對鎖進行獲取的線程一定先拿到鎖,那麼這個鎖是公平的,反之就是不公平的。

比如:排隊買包子,大家都一一排隊進行購買那麼就是公平的,但是如果有人插隊,那就變成不公平了。憑啥你這個後來的還先買包子,就這個意思拉~~

4.3 lock方法

以下就是一個簡單鎖的演示了,簡單的加鎖解鎖。 

public class ReentrantLockTest {

    public static void main(String[] args) {
        // 創建公平鎖
        ReentrantLock lock = new ReentrantLock(true);
        // 加鎖
        lock.lock();
        hello();
        // 解鎖
        lock.unlock();
    }

    public static void hello() {
        System.out.println("Say Hello");
    }
}

既然我們是看加鎖的過程,就從lock方法開始下手唄,前方高能,請注意準備~~~

點進去之後看到了調用了sync對象的lock方法,sync是我們ReentrantLock中的一個內部類,並且這個sync繼承了AQS這個類。

 public void lock() {
        sync.lock();
    }
 abstract static class Sync extends AbstractQueuedSynchronizer {
        private static final long serialVersionUID = -5179523762034025860L;

        // 抽象方法,由公平鎖和非公平鎖具體實現
        abstract void lock();
        
        // ..... 代碼省略

    }

通過快捷鍵查看,有兩個類對Sync中的lock方法進行了實現,我們先看公平鎖:FairSync

看代碼得知,lock方法最後調用了acquire方法,並且傳入了一個參數,值爲:1,那我們再繼續跟下去~ 

 /**
     * Sync object for fair locks
     */
    static final class FairSync extends Sync {
        private static final long serialVersionUID = -3000897897090466540L;

        final void lock() {
            acquire(1);
        }

        //  ...... 代碼省略
    }

這個時候我們就來到AQS爲我們提供的方法了,接下來小編一一講解~~ 

  public final void acquire(int arg) {
        // 第一個調用了tryAcquire方法,這方法判斷能不能拿到鎖
        // 強調,這裏的tryAcquire的結果,最後是取反,最前面加了 !運算
        if (!tryAcquire(arg) &&
            // 後面的方法,慢慢道來,先保持神祕感 
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }

4.3 tryAcquire方法

從方法名上看,字面意思就是嘗試獲取,獲取什麼呢? 那當然是獲取鎖呀。

從acquire()點擊tryAcquire方法進去看,AQS爲我們提供了默認實現,默認如果沒重寫該方法,則拋出一個異常,這裏就很突出模板方法模式這種設計模式的概念,提供了一個默認實現。

 protected boolean tryAcquire(int arg) {
        throw new UnsupportedOperationException();
    }

同樣,我們查看公平鎖的實現  ~ 

最後來到了FairSync對象中的tyrAcquire方法了,重點來啦~~

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

        final void lock() {
            acquire(1);
        }
        
        // 嘗試獲取鎖 拿到鎖了返回:true,沒拿到鎖返回:false
       protected final boolean tryAcquire(int acquires) {
            // 獲取當前線程
            final Thread current = Thread.currentThread();
            // 獲取鎖狀態 , 自由狀態 = 0,被上鎖 = 1 ,> 1 表示重入
            int c = getState();
            // 判斷當前狀態是否等於自由狀態
            if (c == 0) {
                // hasQueuedPredecessors 判斷自己需不需要排隊,這個方法比較複雜,在下面補充部分詳細解釋,返回值,不需要排隊返回false,然後取反,需要排隊返回true
                if (!hasQueuedPredecessors() &&
                   // compareAndSetState 如果不需要排隊則直接進行CAS嘗試加鎖,成功則直接方法true
                    compareAndSetState(0, acquires)) {
                    // 成功獲取鎖,把當前線程設置成鎖的擁有者,爲了後續方便判斷是不是可重入鎖    
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
            // 判斷當前線程是否等於鎖的持有線程,這裏也證明了ReentrantLock是可重入鎖
            else if (current == getExclusiveOwnerThread()) {
                // 如果是重複鎖,計數器 + 1
                int nextc = c + acquires;
                // 正常來說nextc不可能會小於0,於是判斷如果小於0則直接拋出異常
                if (nextc < 0)
                    throw new Error("Maximum lock count exceeded");
                // 賦值計數器+1的結果    
                setState(nextc);
                // 如果重入成功返回true
                return true;
            }
             // 如果c不等於0,並且當前線程不等於持有鎖的線程,直接返回false,因爲就代表着有其他線程拿到鎖了
            return false;
        }
    }

tryAcquire方法執行完成,又回到這裏: tryAcquire方法拿到鎖返回結果:true,沒拿到鎖返回:false。

一共分兩種情況:
       第一種情況,拿到鎖了,結果爲true,通過取反,最後結果爲false,由於這裏是 && 運算,後面的方法則不會進行,直接返回,代碼正常執行,線程也不會進入阻塞狀態。

第二種情況,沒有拿到鎖,結果爲false,通過取反,最後結果爲true,這個時候,if判斷會接着往下執行,執行這句代碼:acquireQueued(addWaiter(Node.EXCLUSIVE), arg),先執行addWaiter方法。

  public final void acquire(int arg) {
        // tryAcquire執行完,回到這裏
        if (!tryAcquire(arg) &&
            // Node.EXCLUSIVE 這裏傳進去的參數是爲null,在Node類裏面
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }

4.4 addWaiter方法

看到這裏,還記得我們AQS裏面有個head、tail,以及Node吧,如果印象模糊了,趕緊翻上去看看。

AQS在初始化的時候,數據大概是這個樣子的,這個時候隊列還沒初始化的狀態,所以head、tail都是爲空。

 addWaiiter這個方法總的來說做了什麼事呢?

核心作用:把沒有獲取到的線程包裝成Node節點,並且添加到隊列中

具體邏輯分兩步,判斷隊列尾部節點是不是爲空,爲空就去初始化隊列,不爲空就維護隊列關係。

這裏需要小夥伴掌握雙向鏈表數據結構,才能更容易的明白怎麼去維護一個隊列的關係。

private Node addWaiter(Node mode) {
    // 因爲在AQS隊列裏面,節點元素是Node,所以需要把當前類包裝成一個node節點
    Node node = new Node(Thread.currentThread(), mode);
    // 把尾節點,賦值給pred,這裏一共分兩種兩種情況
    Node pred = tail;
    // 判斷尾部節點等不等於null,如果隊列沒有被初始化,tail肯定是個空
    // 反而言之,如果隊列被初始化了,head和tail都不會爲空
    if (pred != null) {
         // 整個就是維護鏈表關係
        // 把當前需要加入隊列元素的上一個節點,指向隊列尾部
        node.prev = pred;
        // CAS操作,如果隊列的尾部節點是等於pred的話,就把tail 設置成 node,這個時候node就是最後一個節點了
        if (compareAndSetTail(pred, node)) {
            // 把之前尾部節點的next指向最後的的node節點
            pred.next = node;
            return node;
        }
    }
    // 初始化隊列
    enq(node);
    return node;
}

到這裏小夥伴要記住:AQS隊列默認是沒有被初始化的,只有當發生競爭的時候,並且有線程沒有拿到鎖纔會初始化隊列,否則隊列不會被初始化~

什麼情況下不會被初始化呢?

1、線程沒有發生競爭的情況下,隊列不會被初始化,由tryAcquire方法就可以體現出,如果拿到鎖了,就直接返回了。

2、線程交替執行的情況下,隊列不會被初始化,交替執行的意思是,線程執行完代碼後,釋放鎖,線程二來了,可以直接獲取鎖。這種就是交替執行,你用完了,正好就輪到我用了。

4.5 enq方法

這個方式就是爲了初始化隊列,參數是由addWaiter方法把當前線程包裝成的Node節點。

// 整個方法就是初始化隊列,並且把node節點追加到隊列尾部
private Node enq(final Node node) {
    // 進來就是個死循環,這裏看代碼得知,一共循環兩次
    for (;;) {
        Node t = tail;
        // 第一次進來tail等於null
        // 第二次進來由於下面代碼已經把tail賦值成一個爲空的node節點,所以t現在不等於null了
        if (t == null) {
            // CAS把head設置成一個空的Node節點
            if (compareAndSetHead(new Node()))
                // 把空的頭節點賦值給tail節點
                tail = head;
        } else {
            // 第二次循環就走到這裏,先把需要加入隊列的上一個節點指向隊列尾部
            node.prev = t;
            // CAS操作判斷尾部是不是t如果是,則把node設置成隊列尾部
            if (compareAndSetTail(t, node))  {
                // 再把之前鏈表尾部的next屬性,連接剛剛更換的node尾部節點
                t.next = node;
                return t;
            }
        }
    }
}

通過enq代碼我們可以得知一個很重要、很重要、很重要的知識點,在隊列被初始化的時候,知道隊列第一個元素是什麼麼? 如果你認爲是要等待線程的node節點,那麼你就錯了。

通過這兩句代碼得知,在隊列初始化的時候,是new了一個空Node節點,賦值給了head,緊接着,又把head 賦值給tail。

 if (compareAndSetHead(new Node()))
                // 把空的頭節點賦值給tail節點
                tail = head;

初始化完成後,隊列結構應該是這樣子的。 

隊列初始化後,緊接着第二次循環對不對,t就是我們的尾部節點,node就是要被加入隊列的node節點,也就是我們所謂要等待的線程的node節點,這裏代碼執行完後,直接return了,循環終止了。

 // 第二次循環就走到這裏,先把需要加入隊列的上一個節點指向隊列尾部
            node.prev = t;
            // CAS操作判斷尾部是不是t如果是,則把node設置成隊列尾部
            if (compareAndSetTail(t, node))  {
                // 再把之前鏈表尾部的next屬性,連接剛剛更換的node尾部節點
                t.next = node;
                return t;
            }

看了這幅圖,哪怕對雙向鏈表不熟悉,應該也可以看懂了吧, skr skr skr ~~~~ 

記住,這裏隊列初始化的時候,第一個元素是空,隊列裏面存在兩個元素,切記切記切記,這也是面試需要注意的細節,把這個點勇敢的、大聲的、自信的出說來,肯定能夠證明你是看過源碼的。

好了,最終addWaiter方法會返回一個初始化並且已經維護好,隊列關係的Node節點出來。

  public final void acquire(int arg) {
        if (!tryAcquire(arg) &&
            // addWaiter返回Node,緊接着調用acquireQueued 方法
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }

4.5 acquireQueued方法

看到這裏,也就是我們lock方法的接近尾聲了,我們拿到了隊列中的數據,猜猜接下來需要做什麼?

既然沒拿到鎖,就讓線程進入阻塞狀態,但是肯定不是直接就阻塞了,還需要經過一系列的操作,看源碼:

// node == 需要進隊列的節點、arg = 1
final boolean acquireQueued(final Node node, int arg) {
    // 一個標記
    boolean failed = true;
    try {
        // 一個標記
        boolean interrupted = false;
        // 又來一個死循環
        for (;;) {
            // 獲取當前節點的上一個元素,要麼就是爲頭部節點,要麼就是其他排隊等待的節點
            final Node p = node.predecessor();
            // 判斷是不是頭部節點,如果是頭部節點則代表當前節點是排隊在第一個,有資格去獲取鎖,也就是自旋獲取鎖
            // 如果不是排隊在第一個,前面的人還在排隊後面的就更別說了去獲取鎖了
            // 就比如食堂打飯,有人正在打飯,而排隊在第一個人的可以人,可以去看看前面那個人打完了沒
            // 因爲他前面沒有沒人排隊,而後面的人就不同,前面的人還在排隊,那麼自己就只能老老實實排隊了
            if (p == head && tryAcquire(arg)) {
                // 如果能進入到這裏,則代表前面那個打飯的人已經搞完了,可以輪第一個排隊的人打飯了
                // 既然前面那個人打完飯了,就可以出隊列了,會把thread、prev、next置空,等待GC回收
                setHead(node);
                p.next = null; // help GC
                failed = false;
                // 返回false,整個acquire方法返回false,就出去了
                return interrupted;
            }
            // 如果不是頭部節點,就要過來等待排隊了
            // shouldParkAfterFailedAcquire 這方法會使當前循環再循環一次,相當於自旋一次獲取鎖
            if (shouldParkAfterFailedAcquire(p, node) &&
                // 隊列阻塞,整個線程就等待被喚醒了 
                parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

看代碼得知,如果當前傳進來的節點的上一個節點,是等於head,那麼又會調用tryAcquire方法,這裏體現的就是自旋獲取鎖,爲什麼要這麼做呢? 是爲了避免進入阻塞的狀態,假設線程一已經獲取到鎖了,然後線程二需要進入阻塞,但是由於線程二還在進入阻塞狀態的路上,線程一就已經釋放鎖了。爲了避免這種情況,第一個排隊的線程,有必要在阻塞之前再次去嘗試獲取鎖。

假設一:假設我們線程二在進入阻塞狀態之前,嘗試去獲取鎖,哎,竟然成功了,則會執行一下代碼:

  // 調用方法,代碼在下面
  setHead(node);
  p.next = null; // help GC

   private void setHead(Node node) {
        head = node;
        node.thread = null;
        node.prev = null;
    }

如果拿到鎖了,隊列的內容,當然會發送變化,由圖可見,我們會發現一個問題,隊列的第一個節點,又是一個空節點。

因爲當拿到鎖之後,會把當前節點的內容,指針全部賦值爲null,這也是個小細節喲。

假設二如果當前節點的上一個節點,不是head,那麼很遺憾,沒有資格去嘗試獲取鎖,那就走下面的代碼。

在進入阻塞之前,會調用shouldParkAfterFailedAcquire方法,這個方法小編先告訴你,由於我們這裏是死循環對吧,這個方法第一次調用會放回false,返回false則不會執行執行後續代碼,再一次進入循環,經過一些列操作,還是沒有資格獲取鎖,或者獲取鎖失敗,則又會來到這裏。當第二次調用shouldParkAfterFailedAcquire方法,會放回ture,這個時候,線程纔會調用parkAndCheckInterrupt方法,將線程進入阻塞狀態,等待鎖釋放,然後被喚醒!!

 // 如果不是頭部節點,就要過來等待排隊了
            // shouldParkAfterFailedAcquire 這方法會使當前循環再循環一次,相當於自旋一次獲取鎖
            if (shouldParkAfterFailedAcquire(p, node) &&
                // 隊列阻塞,整個線程就等待被喚醒了 
                parkAndCheckInterrupt())
                interrupted = true;

4.5 parkAndCheckInterrupt方法

  private final boolean parkAndCheckInterrupt() {
        // 在這裏被park,等待unpark,如果該線程被unpark,則繼續從這裏執行
        LockSupport.park(this);
        // 這個是獲取該線程是否被中斷過,這句代碼需要結合lockInterruptibly方法來講,小編就不詳細說了,不然一篇文章講太多了~~~~
        return Thread.interrupted();
    }

到這裏我們ReentrantLock整個加鎖的過程,就相當於講完啦,但是這纔是最最最簡單的一部分,因爲還有很多場景沒考慮到。

4.6 補充說明:shouldParkAfterFailedAcquire方法

上面說爲什麼這個方法第一次調用返回false,第二次調用返回ture,我們來看源碼吧~~

這個方法主要做了一件事:把當前節點的,上一個節點的waitStatus狀態,改爲 - 1。

當線程進入阻塞之後,自己不會把自己的狀態改爲等待狀態,而是由後一個節點進行修改。 細節、細節、細節

舉個例子:你躺在牀上睡覺,然後睡着了,這個時候,你能告訴別人你睡着了嗎? 當然不行,因爲你已經睡着了,呼嚕聲和打雷一樣,怎麼告訴別人。 只有當後一個人來了,看到你在呼呼大睡,它纔可以告訴別人你在睡覺。

//pred 當前上一個節點,node 當前節點
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
    // 第一次循環進來:獲取上一個節點的線程狀態,默認爲0
    // 第二次循環進來,這個狀態就變成-1了,
    int ws = pred.waitStatus;
    // 判斷是否等於-1,第一進來是0,並且會吧waitStatus狀態改成-1,代碼在else
    // 第二次進來就是-1了,直接返回true,是當前線程進行阻塞
    if (ws == Node.SIGNAL)
        /*
         * This node has already set status asking a release
         * to signal it, so it can safely park.
         */
        return true;
    // 判斷是否大於0,waitStatus分幾種狀態,這裏其他幾種狀態的源碼就不一一講了。
    // = 1:由於在同步隊列中等待的線程,等待超時或者被中斷,需要從同步隊列中取消等待,該節點進入該狀態不會再變化
    // = -1:後續節點的線程處於等待狀態,而當前節點的線程如果釋放了同步狀態或者被取消,將會通知後續節點,使後續節點繼續運行
    // = -2:節點在等待隊列中,節點線程在Condition上,當其他線程對Condition調用了signal()方法後,該節點將會從等待隊列中轉移到同步隊列中,加入到同步狀態中獲取
    // = -3:表示下一次共享式同步狀態獲取將會無條件地被傳播下去
    // = 0 :初始狀態
    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;
    // 因爲默認0,所以第一次會走到else方法裏面    
    } else {
        // CAS吧waitStatus修改成-1
        compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
    }
    // 返回false,外層方法接着循環操作
    return false;
}

五、ReentrantLock 解鎖過程源碼分析

5.1 unlock方法

講完加鎖過程,就來解鎖過程吧,說實話,看源碼這種經歷,必須要自己花時間去看,去做筆記,去理解,大腦最好有個整體的思路,這樣纔會印象深刻。

public class ReentrantLockTest {

    public static void main(String[] args) {
        // 創建公平鎖
        ReentrantLock lock = new ReentrantLock(true);
        // 加鎖
        lock.lock();
        hello();
        // 解鎖
        lock.unlock();
    }

    public static void hello() {
        System.out.println("Say Hello");
    }
}

點擊unlock解鎖的方法,會調用到release方法,這個是AQS提供的模板方法,再來看tryRelease方法。

  public void unlock() {
        sync.release(1);
    }
public final boolean release(int arg) {
    // tryRelease 釋放鎖,如果真正釋放會把當前持有鎖的線程賦值爲空,否則只是計數器-1
    if (tryRelease(arg)) {
        Node h = head;
        if (h != null && h.waitStatus != 0)
            unparkSuccessor(h);
        return true;
    }
    return false;
}

5.1 tryRelease方法

發現又是個抽象類,我們選擇ReentrantLock類實現的

這裏要注意:

1、當前解鎖的線程,必須是持有鎖的線程

2、state狀態,必須是等於0,纔算是真正的解鎖,否則只是代表重入次數-1. 

protected final boolean tryRelease(int releases) {
   // 獲取鎖計數器 - 1
    int c = getState() - releases;
    // 判斷當前線程 是否等於 持有鎖的線程,如果不是則拋出異常
    if (Thread.currentThread() != getExclusiveOwnerThread())
        throw new IllegalMonitorStateException();
    // 返回標誌
    boolean free = false;
    // 如果計算器等於0,則代表需要真正釋放鎖,否則是代表重入次數-1
    if (c == 0) {
        free = true;
        // 將持有鎖的線程賦值空
        setExclusiveOwnerThread(null);
    }
    // 重新設置state狀態
    setState(c);
    return free;
}

執行完tryRelease方法,返回到release,進行if判斷,如果返回false,就直接返回了,否則進行解鎖操作。

public final boolean release(int arg) {
    // tryRelease方法返回true,則表示真的需要釋放鎖
    if (tryRelease(arg)) {
        // 如果是需要真正釋放鎖,先獲取head節點
        Node h = head;
        // 第一種情況,假設隊列沒有被初始化,這個時候head是爲空的,則不需要進行鎖喚醒
        // 第二種情況,隊列被初始化了head不爲空,並且只要有線程在隊列中排隊,waitStatus在被加入隊列之前,會把當前節點的上一個節點的waitStatus改爲-1
        // 所以只有滿足h != null && h.waitStatus != 0 這個條件表達式,才能真正代表有線程正在排隊
        if (h != null && h.waitStatus != 0)
            // 解鎖操作,傳入頭節點信息
            unparkSuccessor(h);
        return true;
    }
    return false;
}

5.1 unparkSuccessor方法

這裏的參數傳進來的是head的node節點信息,真正解鎖的線程是head.next節點,然後調用unpark進行解鎖。

private void unparkSuccessor(Node node) {
    // 先獲取head節點的狀態,應該是等於-1,原因在shouldParkAfterFailedAcquire方法中有體現
    int ws = node.waitStatus;
    // 由於-1會小於0,所以重新改爲0
    if (ws < 0)
        compareAndSetWaitStatus(node, ws, 0);

     // 獲取第一個正常排隊的隊列
     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);
}

如果這裏調用unpark,線程被喚醒,會接着這個方法接着執行。到這裏整個解鎖的過程小編就講完了。

  private final boolean parkAndCheckInterrupt() {
        // 在這裏被park,等待unpark,如果該線程被unpark,則繼續從這裏執行
        LockSupport.park(this);
        // 這個是獲取該線程是否被中斷過,這句代碼需要結合lockInterruptibly方法來講,小編就不詳細說了,不然一篇文章講太多了~~~~
        return Thread.interrupted();
    }

 

六、最後難點:補充說明:hasQueuedPredecessors方法

看到這裏,小編希望是小夥伴真的理解了ReentrantLock加鎖和解鎖的過程,並且在心裏有整體流程,不然你看這個方法,會很蒙,這個方法雖然代碼幾行,但是要完全理解,比較困難。

這個方法估計是ReentrantLock加鎖過程中,最爲複雜的一個方法了,所以放到了最後來講~~~

// 不要小看以下幾行代碼,涉及的場景比較複雜
public final boolean hasQueuedPredecessors() {
   // 分別把尾節點、頭節點賦值給 t、h
    Node t = tail; 
    Node h = head;
    Node s;
    // AQS隊列如果沒有發生競爭,剛開始都是未初始化的,所以一開始tail、head都是爲null
    // 第一種情況: AQS隊列沒有初始化的情況
    // 假設線程一,第一個進來,這個時候t、h都是爲null,所以在h != t,這個判斷返回false,由於用的是&&所以整個判斷返回fase
   // 返回flase表示不需要排隊。
   // 但是也不排除可能會有兩個線程同時進來判斷的情況,假設兩個線程發現自己都不需要排隊,就跑去CAS進行修改計數器,這個時候肯定會有一個失敗的
   // CAS 是可以保證原子性操作的,假設線程一它CAS成功了,那麼線程二就會去初始化隊列,老老實實排隊去了
    
   // 第二種情況: AQS隊列被初始化了
   // 場景一:隊列元素大於1個點情況,假設有一個線程在排隊,在隊列中應該有而個元素,一個是頭節點、線程2
   // 現在線程2之前的線程已經執行完,並且釋放鎖喚醒線程2.線程2又會繼續醒來循環。並且線程二是第一個排隊的,所以有資格獲取鎖
   // 只要是獲取鎖就會來排隊需不需要排隊,代碼又回到這裏
   // 現在 h = 等於頭節點,而 tail = 線程2的node節點,所以 h != t 結果爲true
   // h表示頭節點,並且h.next是線程2的節點,所以 (s = h.next) == null 返回 flase
   // s 等於 h.next,也就是線程2的節點信息,並且當前執行的線程也是線程2,所以 s.thread != Thread.currentThread(),返回false
   // 最後return的結果是 true && false,結果爲false,代表不需要排隊
   
   // 場景二:隊列元素等於1個,什麼情況下隊列被初始化了並且只有一個元素呢?
   // 當有線程競爭初始化隊列,之後隊列又全部都被消費完了。最後剩下一個爲空的node,並且head和tail都指向它
   // 這個時候有新的線程進來,其實h != t,直接返回false,因爲head 和tail都指向最後一個節點了
   
    return h != t &&
        ((s = h.next) == null || s.thread != Thread.currentThread());
}

小編按照自己的思路,儘自己有限的能力,表示了出來。 

至於要理解,真的需要小夥伴仔細去思考,不斷的理解,纔會形成自己的思路, 奧利給~~~~~

這裏整個ReentrantLock源碼加鎖解鎖的流程圖,小編也沒貼出來,靠小夥伴們去完成吧~~~~~

七、程序人生,你怎麼看?

這篇文章說了這麼多,看了這麼多源碼,但實際上纔是ReentrantLock很基本的東西。

這就引起一個思考,一個小小的ReentrantLock想要完完全全的去精通每行代碼,需要我們花大量的時間、精力去研究,去探討。 更何況作爲一名程序員,所需要掌握的技術,你們懂得,好像看不到盡頭。 真的就是,你知道的越多,就會發現你不知道得就越多。

對於正在走向程序員道路上的你們,或者已經碼代碼幾年的程序員們,你們是選擇繼續堅持,和代碼死磕到底,一直到年齡的瓶頸、還是中途就選擇轉行呢? 可以文章尾部評論喲。 

點贊是你們給博主最大的動力喲 👇👇👇👇👇👇

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章