【併發編程系列5】一行一行從源碼深入分析ReentrantLock和AQS同步隊列實現原理

前言

鎖是一種用來控制多線程訪問共享資源的工具。通常,鎖可以獨佔共享資源:同一時間只有一個線程可以獲得鎖,並且所有訪問共享資源的線程都必須首先獲得鎖。前面我們介紹過了synchronized,使用synchronized的方法和代碼塊作用域機制使得使用監視器鎖更加簡單,並且幫助避免了許多關於鎖的常見編程錯誤,比如鎖未及時釋放等問題。但是有時候我們需要更靈活的使用鎖資源,例如,一些遍歷併發訪問的數據結構的算法需要使用“手動”方法,或者“鎖鏈”:你先獲得節點A的鎖,然後是節點B,然後釋放A獲得C,再釋放B獲得D,以此類推。這種方式如果要使用synchronized就不是很好實現,但是有了Lock就不一樣了,Lock接口允許以不同的範圍去獲取和釋放鎖,並且允許同時獲得多把鎖,也可以以任意的順序釋放。

Lock

Lock在J.U.C 中是最核心的組件,Lock是一個接口,它定義了釋放鎖和獲得鎖的抽象方法,今天我們要分析的ReentrantLock就實現了Lock接口,Lock接口中主要定義了5個方法:

方法 描述
void lock() 獲取鎖,該方法不會響應中斷。如果獲取鎖失敗,當前線程將被阻塞直到成功獲得鎖
void lockInterruptibly() throws InterruptedException 獲取鎖,會響應中斷。其他和lock()方法一樣
boolean tryLock() 非阻塞獲取鎖,該方法需要立即返回獲取鎖結果,成功返回true,失敗返回false
boolean tryLock(long time, TimeUnit unit) throws InterruptedException 指定時間內獲取鎖,會響應中斷:
1.在指定的超時時間內獲得鎖成功則返回true;
2.在超時時間內被中斷則立刻返回獲得鎖結果;
3.在指定超時時間結束後立刻返回獲得鎖結果
void unlock() 釋放鎖。如果沒有獲得鎖則會拋異常
Condition newCondition()

初識ReentrantLock

ReentrantLock,重入鎖。表示支持重新進入的鎖,也就是說,如果當前線程 t1 通過調用 lock方法獲取了鎖之後,再次調用 lock,是不會再阻塞去獲取鎖的,直接增加重入次數就行了(synchronized也是支持重入的)。

ReentrantLock基本使用

ReentrantLock的使用非常簡單,下面就是一個使用的例子:

package com.zwx.concurrent;

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class ReentrantLockDemo {
    public static void main(String[] args) {
        Lock lock = new ReentrantLock();
        lock.lock();
        try {
            System.out.println(1);
        }finally {
            lock.unlock();
        }
    }
}

使用Lock的時候尤其要注意,當加鎖和解鎖發生在不同地方時,必須小心確保所有的持有鎖的代碼都受到try-finally或try-catch代碼塊的保護,用以確保在必要時鎖得到釋放,這也是靈活使用所帶來的代價,因爲使用synchronized時,不需要考慮這個問題。

ReentrantLock原理

假如我們是Lock的設計者,那我們想一下,如果多個線程都來併發搶佔同一把鎖資源時,而同一時間肯定只有一個線程能搶佔成功,那麼搶佔失敗的線程怎麼辦?答案很簡單,肯定是找個地方存起來,但是具體要怎麼存,這個就是個值得思考的問題。ReentrantLock中採用的是一個同步隊列的數據結構來存儲阻塞等待的線程。

同步隊列(AQS)

同步隊列,全稱爲:AbstractQueuedSynchronizer,又可以簡稱爲AQS。在Lock中,這是一個非常重要的核心組件,J.U.C工具包中很多地方都使用到了AQS,所以,如果理解了AQS,那麼再去理解ReentrantLock,Condition,CountDownLatch等工具的實現原理,就會非常輕鬆。

AQS的兩種功能

從使用層面來說,AQS 的功能分爲兩種:獨佔和共享。

  • 獨佔鎖:每次只有一個線程持有鎖,如:ReentrantLock 就是以獨佔方式實現的互斥鎖
  • 共享鎖:允許多個線程同時獲取鎖 ,併發訪問共享資源 , 如:ReentrantReadWriteLock

AQS 的內部實現

AQS依賴內部的一個FIFO雙向隊列來完成同步狀態的管理,當前線程獲取鎖失敗時,AQS會將當前線程以及等待狀態等信息構造成爲一個節點(Node對象)並將其加入AQS中,同時會阻塞當前線程,當鎖被釋放時,會把首節點中的線程喚醒,使其再次嘗試獲取同步狀態。AQS中有一個頭(head)節點和一個尾(tail)節點,中間每個節點(Node)都有一個prev和next指針指向前一個節點和後一個節點,如下圖:
在這裏插入圖片描述

Node對象組成

AQS中每一個節點就時一個Node對象,並且通過節點中的狀態等信息來控制隊列,Node對象是AbstractQueuedSynchronizer對象中的一個靜態內部類,下面就是Node對象的源碼:

 static final class Node {
        static final Node SHARED = new Node();
        static final Node EXCLUSIVE = null;
        static final int CANCELLED =  1;//表示當前線程狀態是取消的
        static final int SIGNAL    = -1;//表示當前線程正在等待鎖
        static final int CONDITION = -2;//Condition隊列有使用到,暫時用不到
        static final int PROPAGATE = -3;//CountDownLatch等工具中使用到,暫時用不到
        volatile int waitStatus;//節點中線程的狀態,默認爲0
        volatile Node prev;//當前節點的前一個節點
        volatile Node next;//當前節點的後一個節點
        volatile Thread thread;//當前節點封裝的線程信息
        Node nextWaiter;//Condition隊列中的關係,暫時用不到
        final boolean isShared() {//暫時用不到
            return nextWaiter == SHARED;
        }

        final Node predecessor() throws NullPointerException {//獲取當前節點的上一個節點
            Node p = prev;
            if (p == null)
                throw new NullPointerException();
            else
                return p;
        }
        Node() {
        }
        Node(Thread thread, Node mode) {//構造一個節點:addWaiter方法中會使用,此時waitStatus默認等於0
            this.nextWaiter = mode;
            this.thread = thread;
        }

        Node(Thread thread, int waitStatus) { //構造一個節點:Condition中會使用
            this.waitStatus = waitStatus;
            this.thread = thread;
        }
    }

上面代碼中加上了註釋,總來來說應該還是比較好理解,注意,Node對象並不是AQS纔會使用,Condition隊列以及其他工具中也會使用,所以有些狀態和方法在這裏是暫時用不上的本文就不會過多關注。

lock.lock()源碼解讀

上面花了一點篇幅介紹了ReentrantLock內部的實現機制,相信大家的腦海裏有了一個初步的輪廓,接下來就讓我們一步步從加鎖到釋放鎖進行源碼解讀吧

ReentrantLock#lock()

上文的示例中,當我們調用lock.lock()時,我們進入Lock接口的實現類ReentrantLock中的加鎖入口:
在這裏插入圖片描述
然後發現這裏調用了sync中的一個lock()方法,sync是ReentrantLock類當中的一個組合類,我們知道,AQS是一個同步隊列,但是因爲AQS只是一個同步隊列,並不具備一些業務執行能力,所以通過了另一個Sync來繼承AbstractQueuedSynchronizer,並根據不同的業務場景來實現不同的業務邏輯:
在這裏插入圖片描述
進入Sync類,我們發現,Sync的lock()方法是一個抽象方法,也就是說我們還需要去執行Sync中的實現類,Sync有兩個實現類:FairSync和NonfairSync,也就是公平鎖和非公平鎖。
在這裏插入圖片描述
公平鎖和非公平鎖其實大致邏輯都是差不多,唯一的區別是非公平鎖在搶佔鎖之前會先判斷一下當前AQS隊列中是不是有線程正在等待,如果沒有才會通過CAS操作去搶佔鎖,相信看完這篇文章,大家自己去看也會很容易理解。這裏我們就以非公平鎖爲例,進入非公平鎖中的lock()方法繼續我們的源碼之旅:

NonfairSync#lock()

在這裏插入圖片描述
這個方法很簡短,首先通過一個CAS操作,如果CAS成功則表示當前線程獲得鎖成功,獲取鎖成功之後,需要在AQS當中記錄一下當前獲得鎖的線程信息:
在這裏插入圖片描述

CAS操作

CAS操作是多線程當中的一個原子操作。
在這裏插入圖片描述
上面代碼的意思是,如果當前內存中的stateOffset的值和預期值expect相等,則替換爲 update值。更新成功返回true,否則返回false。這個操作是原子的,不會出現線程安全問題。
stateOffset是AQS中的一個屬性,它在不同的實現中所表達的含義不一樣,對於重入鎖的實現來說,表示一個同步狀態。它有兩個含義:

  1. 當 state=0 時,表示無鎖狀態
  2. 當 state>0 時,表示已經有線程獲得了鎖,也就是state=1,但是因爲ReentrantLock允許重入,所以同一個線程多次獲得同步鎖的時候,state會遞增,比如重入 5 次,那麼state=5。而在釋放鎖的時候,同樣需要釋放5次直到state=0其他線程纔有資格獲得鎖。
    至於Unsafe類中都是一些本地(native)方法,就不過多敘述了。

CAS操作的ABA問題

CAS操作是一個原子操作,但是CAS操作同樣會存在問題,這就是經典的ABA問題。假如一個值一開始是A,然後被修改成B,最後又被改回了A,那麼這時候有其他線程過來,會發現預期值還是是A,就會以爲沒變(實際上變了:A-B-A),這時候就會CAS操作成功,解決這個問題的辦法就是可以加入一個版本號,比如原始值設置爲A1(1表示版本號),改成B的時候版本號也同時遞增,修改成B2,這時候再次改回A,就變成A3,那麼A1和A3就不相同了,可以避免了ABA問題的產生。

AQS#acquire(arg)

上面的操作中,我們假如線程A是第一次進來,那麼肯定獲得鎖成功,這時候我們可以得到這樣的一個AQS:
在這裏插入圖片描述
這時候AQS隊列還未構造,僅僅只是設置了一些獨佔線程相關的屬性。上圖中表示當前對象已經有線程ThreadA獲得鎖了,這時又來了個線程B,會去執行CAS操作,因爲線程A已經獲得鎖了,狀態已經等於1了,所以CAS失敗,這時候線程B就會調用AQS當中的acquire方法(參數1是固定死的,表示加鎖次數):
在這裏插入圖片描述
這裏也是一個if判斷,我們先看第一個條件,tryAcquire(arg)方法,這裏我們同樣進入非公平鎖實現NonfairSync類中,然後會發現最終其還是會調用父類Sync中的方法nonfairTryAcquire(arg)

Sync#nonfairTryAcquire(arg)

在這裏插入圖片描述
這個方法就是爲了嘗試去獲得鎖,但是這裏有一個問題,因爲這個方法之所以會被執行,就是因爲前面的CAS操作失敗了,也就是獲得鎖失敗了,state就肯定不會爲0了,可是這個方法的131行爲什麼還要再次判斷state是否等於0呢?

我們想一想,如果一個線程爭搶鎖失敗,我們應該怎麼做,無外乎兩個辦法:一個就是多試幾次,之前介紹synchronized的時候曾經說過,大部分鎖被持有之後都會很快被釋放,所以再試試總沒有錯,萬一剛好鎖被釋放了呢。另一個辦法就是阻塞等待,這個後面會介紹,所以這裏的131行代碼判斷也是這個邏輯,就是再試一次,如果成功,就可以直接獲得鎖,而不需要加入AQS隊列掛起線程了。

線程A沒有釋放鎖的時候,線程B會搶佔鎖失敗,則返回false,我們回到之前的邏輯,會繼續執行acquireQueued方法,這個方法裏面有一個參數是addWaiter返回的,所以我們先去看addWaiter這個方法

AQS#addWaiter(Node)

走到這裏就是說明當前線程至少2次嘗試獲取鎖都失敗了,所以當前線程會被初始化成爲一個Node節點,加入到AQS隊列中。我們前面提到了,AQS有兩種模式,一種獨佔,一種共享,而重入鎖ReentrantLock是獨佔的,所以這裏固定傳入了參數Node.EXCLUSIVE表示當前鎖是獨佔的。而由前面Node對象的源碼可以知道,Node.EXCLUSIVE其實是一個null值的Node對象。
在這裏插入圖片描述
因爲我們到這裏的時候是第一次進來,AQS隊列還沒有被初始化,head和tail都是爲空的,所以if判斷肯定不成立,也就是說,如果是第一次調用addWaiter方法時,會先執行下面的enq(node)方法。

AQS#enq()

在這裏插入圖片描述
先不要看else邏輯,線程B第一次進來肯定是走的if邏輯,初始化之後,得到這樣的一個AQS:
圖一

頭節點爲什麼放空線程

注意了,這裏的頭節點中thread沒有賦值(thread=null),其實這裏的第一個節點只是起了一個哨兵的作用,這樣就可以免去了後續在查找過程中每次比較是否越界的操作,後面會陸續提到這個哨兵的作用。

回到源碼邏輯來,因爲上面是一個死循環,初始化之後,緊接着會立刻進行第二次for循環,第二次循環的時候tail節點不爲空了,所以會走else邏輯,走完else邏輯之後會得到下面這樣一個AQS:
在這裏插入圖片描述
這時候假如又來了線程C,那麼線程C就會走到AQS#addWaiter(Node)方法中上面的if邏輯了,因爲這時候tail節點已經不爲空了,這裏的if邏輯其實和enq(Node)方法中for循環中的else分支邏輯是一樣的,只是把線程C添加到AQS的尾部,最終會得到下面這個AQS:
在這裏插入圖片描述
接下來我們回到前面的方法,繼續執行AQS中的acquireQueued(Node,arg)方法。

AQS#acquireQueued(Node,arg)

上面經過addWaiter(Node)之後,阻塞的線程已經被加入到了AQS隊列當中,但是注意,這時候僅僅只是把線程加入進去了,而線程並沒有被掛起,也就是說,線程還是處於運行狀態,那麼接下來要做的事就是需要把加入AQS隊列中的線程掛起,當然在掛起之前,還是我們前面說的,就是線程還是不死心,所以還需要最後搏一搏,萬一搶到鎖了,就不需要掛起了,所以這就是acquireQueued(Node,arg)方法中會做的兩件事:
1、看看前一個節點是不是頭節點,如果是的話,就再試一次
2、再試一次如果還是失敗了,那麼線程正式掛起
在這裏插入圖片描述
有幾個屬性這裏可以先不管,關注for循環裏面邏輯,首先獲取到前一個節點,如果前一個節點是head節點,那就再調用tryAcquire(arg)方法去搶一次鎖。
我們這裏假設爭搶鎖還是失敗了,這時候就會走到882行的if判斷,if判斷中第一個邏輯看名字shouldParkAfterFailedAcquire能猜到大致意思,就是爭搶鎖失敗後看一下當前線程是不是應該掛起,我們進入shouldParkAfterFailedAcquire方法看看:
在這裏插入圖片描述
上面這段代碼值得說的就是811-815行,我們先來演示下這個流程,因爲移除cancel狀態節點後面邏輯中還會出現。

1、假設ThreadB被取消了,那麼這時候AQS中ThreadB節點狀態爲-:
在這裏插入圖片描述
2、執行813行代碼,相當於:prev=prev.prev;node.prev=prev;得到如下AQS:
在這裏插入圖片描述
3、這時候while循環的條件肯定不成立,因爲此時的pred已經指向了頭節點,狀態爲-1,
所以循環結束,繼續執行815行代碼,得到如下AQS:
在這裏插入圖片描述
最終的結果我們可以看到,雖然ThreadB還有指向其他線程,但是我們通過其他任何節點,都沒辦法找到ThreadB,已經重新構建了一個關聯關係,相當於ThreadB被移出了隊列。
因爲head節點是一個哨兵,不可能會被取消,所以這裏的while循環是不需要擔心pred會變爲null的。

暫時忘掉上面移除cancel節點的流程,我們假設是線程B進來,那麼前一個節點就是head節點,肯定會走到最後一個else,這也是一個CAS操作,把頭節點狀態改爲-1,如果是線程C進來,就會把B節點設置爲-1,這時候就會得到下面這樣一個AQS:
在這裏插入圖片描述
這個AQS隊列和上面的唯一區別就是前面兩個節點的waitStatus狀態從0改成了-1。

這裏注意了,只有前一個節點waitStatus=-1纔會返回true,所以這裏第一次循環進來肯定返回false,也就是還會再一次進行循環,循環的時候還會再次執行上面的爭搶鎖方法(看起來真的是賊心不死哈)。判斷失敗後,就會二次進入shouldParkAfterFailedAcquire方法,這時候因爲第一次循環已經把前一個節點狀態改爲-1了,所以就會返回true了。

返回true之後,就會執行if判斷的第二個邏輯了,這裏面纔是真的把線程正式掛起來。要掛起一個線程着實有點不容易哈哈。調用parkAndCheckInterrupt()方法正式掛起:
在這裏插入圖片描述

爲什麼要使用interrupted()返回中斷標記

要解釋這個原因我們需要先解釋下park()方法:
LockSupport.park()方法是中斷一個線程,但是遇上下面三種情況,就會立即返回:

  • 其他線程對當前線程發起了unpark()操作時
  • 其他線程中斷了當前線程時
  • 不合邏輯的調用(也就是沒有理由)時
    第三點沒想明白場景,有知道的歡迎留言,感謝!

這裏我們要說的是第2點,其他線程中斷了當前線程會有什麼影響,我們先來演示一個例子再來得出結論:

當park()遇上了interrupt()

前面講線程基本知識的時候,我們講到了sleep()遇到了interrupt()會怎麼樣,感興趣的可以點擊這裏詳細瞭解。
這裏我們來看個例子:

package com.zwx.concurrent.lock;

import java.util.concurrent.locks.LockSupport;

public class LockParkInterrputDemo {

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(()->{
            int i = 0;
            while (true){
               if(i == 0){
                   LockSupport.park();
                   //獲取中斷標記,但是不復位
                   System.out.println(Thread.currentThread().isInterrupted());
                   LockSupport.park();
                   LockSupport.park();
                   System.out.println("如果走到這裏就說明park不生效了");
               }
               i++;
               if(i == Integer.MAX_VALUE){
                   break;
               }
            }
        });
        t1.start();
        Thread.sleep(1000);//確保t1被park()之後再中斷
        t1.interrupt();
        System.out.println("end");
    }
}

輸出結果:
在這裏插入圖片描述
所以其實park()方法至少有以下兩個個特點:

  • 當一個線程park()時收到中斷信號,會立刻恢復,且中斷標記爲true,而且不會拋出InterruptedException
  • 當一個線程中斷標記爲true時候,park()對其無效

有這兩個結論,上面就很好理解了,我們想一想,假設上面的線程掛起之後,並不是被線程A釋放鎖之後調用unpark()喚醒的,而是被其他線程中斷了,那麼就會立刻恢復繼續後面的操作,這時候如果不對線程進行復位,那麼他會回到前面的死循環,park()也無效了,就會一直死循環搶佔鎖,會一直佔用CPU資源,如果線程多了可能直接把CPU耗盡。

分析到這裏,線程被掛起,告一段落。掛起之後需要等待線程A釋放鎖之後喚醒再繼續執行。所以接下來我們看看unlock()是如何釋放鎖以及喚醒後續線程的。

lock.unlock()源碼解讀

ReentrantLock#unlock()

上文的示例中,當我們調用lock.unlock()時,我們進入Lock接口的實現類ReentrantLock中的釋放鎖入口:
在這裏插入圖片描述
這裏和上文的加鎖不一樣,加鎖會區分公平鎖和非公平鎖,這裏直接就是調用了sync父類AQS中的release(arg)方法:
在這裏插入圖片描述
我們可以看到,這裏首先會調用tryRelease(arg)方法,最終會回到ReentrantLock類中的tryRelease(arg)方法:

ReentrantLock#tryRelease()

在這裏插入圖片描述
這個方法看起來就比較簡單了,釋放一次就把state-1,所以我們的lock()和unlock()是需要配對的,否則無法完全釋放鎖,這裏因爲我們沒有重入,所以c=0,那麼這時候的AQS隊列就變成了這樣:
在這裏插入圖片描述

當前方法返回true,那麼就會繼續執行上面AQS#release(arg)方法中if裏面的邏輯了:
在這裏插入圖片描述
這個方法就沒什麼好說的,比較簡單了,我們直接進入到unparkSuccessor(h)方法中一窺究竟。

AQS#unparkSuccessor(Node)

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.
         * 如果狀態是負的,嘗試去清除這個信號,當然,如果清除失敗或者說被其他
         * 等待獲取鎖的線程修改了,也沒關係。
         * 這裏爲什麼要去把狀態修改爲0呢?其實這個線程是要被喚醒的,修不修改都無所謂。
         * 回憶一下上面的acquireQueued方法中調用了shouldParkAfterFailedAcquire
         * 去把前一個節點狀態改爲-1,而在改之前會搶佔一次鎖,所以說這裏的操作
         * 其實並沒有太大用處,可能可以爲爭搶鎖的線程再多一次搶鎖機會,故而成功失敗均不影響
         */
        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.
         * 喚醒後繼節點,通常是next節點,但是如果next節點被取消了或者爲空,那麼
         * 就需要從尾部開始遍歷,將無效節點先剔除
         */
        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)//一直遍歷,直到找到狀態小於等於0的有效節點
                    s = t;
        }
        if (s != null)
            LockSupport.unpark(s.thread);
    }

這段代碼中值得說明的是爲什麼要從tail節點開始循環遍歷。不知道大家對enq()方法中的構造AQS隊列的步驟還有沒有印象,爲了不讓大家翻上去找代碼,我把代碼重新貼下來:
在這裏插入圖片描述
我們看到,不管是if分支還是else分支,cas操作成功之後都只是把tail節點的關係構造出來了,第一個if分支CAS操作後得到下面這樣的情況:
在這裏插入圖片描述
執行else分支的CAS操作之後,可能得到下面這樣的情況:
在這裏插入圖片描述
我們可以發現,上面兩種情況next節點都還沒來得及構造,那麼假如這時候從前面還是遍歷就會出現找不到節點的情況,但是從tail往前就不會有這個問題。

看到這裏忍不住感嘆下,大佬的思維真是達到了一定的高度,寫的代碼完全都是精華。

到這裏釋放鎖完成,下一個線程(ThreadB)也被喚醒了,那麼下一個線程被喚醒後在哪裏呢?還是把上面線程最終掛起的代碼貼出來:
在這裏插入圖片描述
也就是說線程被喚醒後,會繼續執行return語句,返回中斷標記。然後會回到AQS類中的
acquireQueued(Node,arg)方法

回到AQS#acquireQueued(Node,arg)

在這裏插入圖片描述
也就是說會回到上面代碼中的882行的if判斷,不管interrupted是等於true(想成掛起期間被中斷過)還是等於false,都不會跳出當前的for循環,那麼就繼續循環。
因爲被喚醒的線程是ThreadB,所以這時候if判斷成立,而且因爲此時state=0,處於無鎖狀態,tryAcquire(arg)獲取鎖也會成功,這時候AQS又變成了有鎖狀態,只不過獨佔線程由A變成了B:
在這裏插入圖片描述
這時候線程B獲取鎖成功了,所以必然要從AQS隊列中移除,我們進入setHead(node)方法:
在這裏插入圖片描述
我們還是來演示一下這三行代碼:
1、head=node,於是得到如下AQS隊列:
在這裏插入圖片描述
2、node.Thread=null;node.prev=null;得到如下AQS隊列:
在這裏插入圖片描述
3、回到前一個方法,執行setHead(Node)下一行代碼,p.next = null,得到如下最新的AQS:
在這裏插入圖片描述
經過這三步,我們看到,原先的頭節點已經沒有任何關聯關係了,其實在第二步的時候,原先頭節點已經不在隊列中了,執行第三步只是爲了消除其持有的引用,方便被垃圾回收。
到這裏,最終會執行return interrupted;跳出循環,繼續回到前一個方法。

回到AQS#acquire(arg)

在這裏插入圖片描述
這時候假如前面的interrupted返回true的話會執行selfInterrupt()方法:
在這裏插入圖片描述
這裏自己中斷自己的原因就是上面介紹過的,上面捕獲到線程中斷之後只是記錄下了中斷狀態,然後對線程進行了復位,所以這時候這裏需要再次中斷自己,對外界做出響應。

到這裏,整個lock()和unlock()分析就結束了,但是上面acquireQueued方法我們這裏需要再進去看一下,裏面的finally中有一個cancelAcquire(Node)方法。

總結

分析到這裏ReentrantLock和AQS就分析完了,上面的公平鎖和非公平鎖本來也想放在後面介紹的,因爲篇幅有限就不準備再去分析公平鎖了,如果有確實想知道的,可以給我評論留言,謝謝大家。
下一篇,將分析Condition隊列,感興趣的 請關注我,和孤狼一起學習進步

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