圖解AQS原理之ReentrantLock詳解-非公平鎖

概述

併發編程中,ReentrantLock的使用是比較多的,包括以前講的LinkedBlockingQueueArrayBlockQueue的內部都是使用的ReentrantLock,談到它又不能的不說AQS,AQS的全稱是AbstractQueuedSynchronizer,這個類也是在java.util.concurrent.locks下面,提供了一個FIFO的隊列,能夠用於構建鎖的基礎框架,內部經過原子變量state來表示鎖的狀態,當state大於0的時候表示鎖被佔用,若是state等於0時表示沒有佔用鎖,ReentrantLock是一個重入鎖,表如今state上,若是持有鎖的線程重複獲取鎖時,它會將state狀態進行遞增,也就是得到一個信號量,當釋放鎖時,同時也是釋放了信號量,信號量跟隨減小,若是上一個線程尚未完成任務,則會進行入隊等待操做。java

本文分析內容主要是針對jdk1.8版本

約束:文中圖片的ref-xxx表明引用地址node

圖片中的內容prve更正爲prev,因爲文章不是一天寫的因此有些圖片更正了有些沒有。編程

AQS主要字段

/**
 * 頭節點指針,經過setHead進行修改
 */
private transient volatile Node head;

/**
 * 隊列的尾指針
 */
private transient volatile Node tail;

/**
 * 同步器狀態
 */
private volatile int state;

AQS須要子類實現的方法

AQS是提供了併發的框架,它內部提供一種機制,它是基於模板方法的實現,整個類中沒有任何一個abstract的抽象方法,取而代之的是,須要子類去實現的那些方法經過一個方法體拋出UnsupportedOperationException異常來讓子類知道,告知若是沒有實現模板的方法,則直接拋出異常。併發

方法名 方法描述
tryAcquire 以獨佔模式嘗試獲取鎖,獨佔模式下調用acquire,嘗試去設置state的值,若是設置成功則返回,若是設置失敗則將當前線程加入到等待隊列,直到其餘線程喚醒
tryRelease 嘗試獨佔模式下釋放狀態
tryAcquireShared 嘗試在共享模式得到鎖,共享模式下調用acquire,嘗試去設置state的值,若是設置成功則返回,若是設置失敗則將當前線程加入到等待隊列,直到其餘線程喚醒
tryReleaseShared 嘗試共享模式下釋放狀態
isHeldExclusively 是不是獨佔模式,表示是否被當前線程佔用

AQS是基於FIFO隊列實現的,那麼隊列的Node節點又是存放的什麼呢?框架

Node字段信息

字段名 類型 默認值 描述
SHARED Node new Node() 一個標識,指示節點使用共享模式等待
EXCLUSIVE Nodel Null 一個標識,指示節點使用獨佔模式等待
CANCELLED int 1 節點因超時或被中斷而取消時設置狀態爲取消狀態
SIGNAL int -1 當前節點的後節點被park,當前節點釋放時,必須調用unpark通知後面節點,當後面節點競爭時,會將前面節點更新爲SIGNAL
CONDITION int -2 標識當前節點已經處於等待中,經過條件進行等待的狀態
PROPAGATE int -3 共享模式下釋放節點時設置的狀態,被標記爲當前狀態是表示無限傳播下去
0 int 不屬於上面的任何一種狀態
waitStatus int 0 等待狀態,默認初始化爲0,表示正常同步等待,
pre Node Null 隊列中上一個節點
next Node Null 隊列中下一個節點
thread Thread Null 當前Node操做的線程
nextWaiter Node Null 指向下一個處於阻塞的節點

經過上面的內容咱們能夠看到waitStatus實際上是有5個狀態的,雖然這裏面0並非什麼字段,可是他是waitStatus狀態的一種,表示不是任何一種類型的字段,上面也講解了關於AQS中子類實現的方法,AQS提供了獨佔模式和共享模式兩種,可是ReentrantLock實現的是獨佔模式的方式,下面來經過源碼的方式解析ReentrantLock函數

ReentrantLock源碼分析

首先在源碼分析以前咱們先來看一下ReentrantLock的類的繼承關係,以下圖所示:源碼分析

圖片描述

能夠看到ReentrantLock繼承自Lock接口,它提供了一些獲取鎖和釋放鎖的方法,以及條件判斷的獲取的方法,經過實現它來進行鎖的控制,它是顯示鎖,須要顯示指定起始位置和終止位置,Lock接口的方法介紹:ui

方法名稱 方法描述
lock 用來獲取鎖,若是鎖已被其餘線程獲取,則進行等待。
tryLock 表示用來嘗試獲取鎖,若是獲取成功,則返回true,若是獲取失敗(即鎖已被其餘線程獲取),則返回false,也就說這個方法不管如何都會當即返回。在拿不到鎖時不會一直在那等待
tryLock(long time, TimeUnit unit) 和tryLock()相似,區別在於它在拿不到鎖時會等待必定的時間,在時間期限以內若是還拿不到鎖,就返回false。若是若是一開始拿到鎖或者在等待期間內拿到了鎖,則返回true
lockInterruptibly 獲取鎖,若是獲取鎖失敗則進行等到,若是等待的線程被中斷會相應中斷信息。
unlock 釋放鎖的操做
newCondition 獲取Condition對象,該組件和當前的鎖綁定,當前線程只有得到了鎖,才能調用該組件wait()方法,而調用後,當前線程釋放鎖。

ReentrantLock也實現了上面接口的內容,前面講解了不少理論行的內容,接下來咱們以一個簡單的例子來進行探討this

public class ReentrantLockDemo {
    public static void main(String[] args) throws Exception {
        AddDemo runnalbeDemo = new AddDemo();
        Thread thread = new Thread(runnalbeDemo::add);
        thread.start();
        Thread thread1 = new Thread(runnalbeDemo::add);
        thread1.start();
          Thread.sleep(1000);
        System.out.println(runnalbeDemo.getCount());
    }
    
    private static class AddDemo {
        private final AtomicInteger count = new AtomicInteger();
        private final ReentrantLock reentrantLock = new ReentrantLock();

        private void add() {
            try {
                reentrantLock.lock();
                count.getAndIncrement();
            } finally {
//                reentrantLock.unlock();
            }
        }

        int getCount() {
            return count.get();
        }
    }
}
  1. 首先聲明內部類AddDemo,AddDemo的主要做用是將原子變量count進行遞增的操做
  2. AddDemo內部聲明瞭ReentrantLock對象進行同步操做
  3. AddDemo的add方法,進行遞增操做,細心地同窗發現,使用了lock方法獲取鎖,可是沒有釋放鎖,這裏面沒有釋放鎖能夠更讓咱們清晰的分析內部結構的變化。
  4. 主線程開啓了兩個線程進行同步進行遞增的操做,最後讓線程休眠一會輸出累加的最後結果。

ReentrantLock內部提供了兩種AQS的實現,一種公平模式,一種是非公平模式,若是沒有特別指定在構造器中,默認是非公平的模式,咱們能夠看一下無參的構造函數。spa

public ReentrantLock() {
    sync = new NonfairSync();
}

當調用有參構造函數時,指定使用哪一種模式來進行操做,參數爲布爾類型,若是指定爲false的話表明非公平模式,若是指定爲true的話表明的是公平模式,以下所示:

public ReentrantLock(boolean fair) {
    sync = fair ? new FairSync() : new NonfairSync();
}

咱們使用的是非公平模式,後面再來進行分析公平模式,上面也講到了分爲兩種模式,這兩種模式爲FairSyncNonfairSync兩個內部靜態類不可變類,不能被繼承和實例化,這兩個類是咱們今天分析的重點,爲何說是重點呢,這裏講的內容是有關於AQS的,而FairSyncNonfairSync實現了抽象內部類SyncSync實現了AbstractQueuedSynchronizer這個類,這個類就是咱們說的AQS也是主要同步操做的類,下面咱們來看一下公平模式和非公平模式下類的繼承關係,以下圖所示:

非公平模式:

圖片描述

公平模式:

圖片描述
經過上面兩個繼承關係UML來看其實無差異,差異在於內部實現的原理不同,回到上面例子中使用的是非公平模式,那先以非公平模式來進行分析,

假設第一個線程啓動調用AddDemo的add方法時,首先執行的事reentrantLock.lock()方法,這個lock方法調用了sync.lock(),sync就是咱們上面提到的兩種模式的對象,來看一下源碼內容:

public void lock() {
    sync.lock();
}

內部調用了sync.lock(),實際上是調用了NonfairSync對象的lock方法,也就是下面的方法內容。

/**
 * 非公平模式鎖
 */
static final class NonfairSync extends Sync {
    private static final long serialVersionUID = 7316153563782823691L;

    /**
     * 執行鎖動做,先進行修改狀態,若是鎖被佔用則進行請求申請鎖,申請鎖失敗則將線程放到隊列中
     */
    final void lock() {
        if (compareAndSetState(0, 1))
            setExclusiveOwnerThread(Thread.currentThread());
        else
            acquire(1);
    }
    // 繼承自AQS的tryAcquire方法,嘗試獲取鎖操做,這個方法會被AQS的acquire調用
    protected final boolean tryAcquire(int acquires) {
        return nonfairTryAcquire(acquires);
    }
}

咱們看到lock方法首先先對state狀態進行修改操做,若是鎖沒有被佔用則獲取鎖,並設置當前線程獨佔鎖資源,若是嘗試獲取鎖失敗了,則進行acqurie方法的調用,例子中第一個線程當嘗試獲取鎖是內部state狀態爲0,進行修改操做的時候,發現鎖並無被佔用,則得到鎖,此時咱們來看一下內部變化的狀況,以下圖所示:

圖片描述

此時只是將state的狀態更新爲1,表示鎖已經被佔用了,獨佔鎖資源的線程是Thread0,也就是exclusiveOwnerThread的內容,頭節點和尾節點都沒有被初始化,當第二個線程嘗試去獲取鎖的時候,發現鎖已經被佔用了,由於上一個線程並無釋放鎖,因此第二線程直接獲取鎖時獲取失敗則進入到acquire方法中,這個方法是AbstractQueuedSynchronizer中的方法acquire,先來看一下具體的實現源碼以下所示:

public final void acquire(int arg) {
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}

我我的理解acquire方法不間斷的嘗試獲取鎖,若是鎖沒有獲取到則現將節點加入到隊列中,並將當前線程設置爲獨佔鎖資源,也就是獨佔了鎖的意思,別的線程不能擁有鎖,而後若是當前節點的前節點是頭節點話,再去嘗試爭搶鎖,則設置當前節點爲頭節點,並將原頭節點的下一個節點設置爲null,幫助GC回收它,若是不是頭節點或爭搶鎖不成功,則會現將前面節點的狀態設置直到設置爲SIGNAL爲止,表明下面有節點被等待了等待上一個線程發來的信號,而後就掛起當前線程。

咱們接下來慢慢一步一步的分析,咱們先來看一下NonfairSync中的tryAcquire,以下所示:

protected final boolean tryAcquire(int acquires) {
    return nonfairTryAcquire(acquires);
}

它調用的是他的父類方法,也就是ReentrantLockSync中的nonfairTryAcquire方法,這個方法主要就是去申請鎖的操做,來看一下具體源碼:

final boolean nonfairTryAcquire(int acquires) {        //首先是一個被final修飾的方法
    final Thread current = Thread.currentThread();    //獲取當前線程
    int c = getState();                                //獲取state的狀態值
    if (c == 0) {                                    //若是狀態等於0表明線程沒有被佔用
        if (compareAndSetState(0, acquires)) {        //cas修改state值
            setExclusiveOwnerThread(current);        //設置當前線程爲獨佔模式
            return true;
        }
    }
    else if (current == getExclusiveOwnerThread()) {//若是state狀態不等於0則先判斷是不是當前線程佔用鎖,若是是則進行下面的流程。
        int nextc = c + acquires;                    //這個地方就說明重入鎖的原理,若是擁有鎖的是當前線程,則每次獲取鎖state值都會跟隨遞增
        if (nextc < 0) // overflow                    //溢出了
            throw new Error("Maximum lock count exceeded");
        setState(nextc);                            //直接設置state值就能夠不須要CAS
        return true;
    }
    return false;                                    //都不是就返回false
}

經過源碼咱們能夠看到其實他是有三種操做邏輯:

  • 若是state爲0,則表明鎖沒有被佔用,嘗試去修改state狀態,而且將當前線程設置爲獨佔鎖資源,表示得到鎖成功
  • 若是state大於0而且擁有鎖的線程和當前申請鎖的線程一致,則表明重入了鎖,state值會進行遞增,表示得到鎖成功
  • 若是state大於0而且擁有鎖的線程和當前申請鎖的線程不一致則直接返回false,表明申請鎖失敗

當第二個線程去爭搶鎖的時候,state值已經設置爲1了也就是已經被第一個線程佔用了鎖,因此這裏它會返回false,而經過acquire方法內容能夠看到if語句中是!tryAcquire(arg),也就是!false=ture,它會進行acquireQueued(addWaiter(Node.EXCLUSIVE), arg))方法,這個方法裏面又有一個addWaiter方法,從方法語義上能看到是添加等待隊列的操做,方法的參數表明的是模式,Node.EXCLUSIVE表示的是在獨佔模式下等待,咱們先來看一下addWaiter裏面是如何進行操做,以下所示:

private Node addWaiter(Node mode) {
      //首先生成當前線程擁有的節點
    Node node = new Node(Thread.currentThread(), mode);
    // 下面的內容是嘗試快速進行插入末尾的操做,在沒有其餘線程同時操做的狀況
    Node pred = tail;                                            //獲取尾節點
    if (pred != null) {                                            //尾節點不爲空,表明隊列不爲空
        node.prev = pred;                                        //尾節點設置爲當前節點的前節點
        if (compareAndSetTail(pred, node)) {                    //修改尾節點爲當前節點
            pred.next = node;                                    //原尾節點的下一個節點設置爲當前節點
            return node;                                        //返回node節點
        }
    }
    enq(node);                                                    //若是前面入隊失敗,這裏進行循環入隊操做,直到入隊成功
    return node;
}

前面代碼中能夠看到,它有一個快速入隊的操做,若是快速入隊失敗則進行死循環進行入隊操做,固然咱們上面例子中發現隊列實際上是爲空的,也就是pred==null,不能進行快速入隊操做,則進入到enq進行入隊操做,下面看一下enq方法實現,以下所示:

private Node enq(final Node node) {
    for (;;) {                                                    //死循環進行入隊操做,直到入隊成功
        Node t = tail;                                            //獲取尾節點
        if (t == null) { // Must initialize                        //判斷尾節點爲空,則必須先進行初始化
            if (compareAndSetHead(new Node()))                  //生成一個Node,並將當前Node做爲頭節點
                tail = head;                                    //head和tail同時指向上面Node節點
        } else {                                                            
            node.prev = t;                                        //設置入隊的當前節點的前節點設置爲尾節點    
            if (compareAndSetTail(t, node)) {                    //將當前節點設置爲尾節點
                t.next = node;                                    //修改原有尾節點的下一個節點爲當前節點
                return t;                                        //返回最新的節點
            }
        }
    }
}

經過上面入隊操做,能夠清晰的瞭解入隊操做其實就是Node節點的prev節點和next節點以前的引用,運行到這裏咱們應該能看到入隊的狀態了,以下圖所示:

圖片描述

如上圖能夠清晰的看到,此時擁有鎖的線程是Thread0,而當前線程是Threa1,頭節點爲初始化的節點,Ref-707引用地址所在的Node節點操做當前操做的節點信息,入隊操做後並無完成,而是繼續往下進行,此時則進行acquireQueued這個方法,這個方法是不間斷的去獲取已經入隊隊列中的前節點的狀態,若是前節點的狀態爲大於0,則表明當前節點被取消了,會一直往前面的節點進行查找,若是節點狀態小於0而且不等於SIGNAL則將其設置爲SIGNAL狀態,設置成功後將當前線程掛起,掛起線程後也有可能會反覆喚醒掛起操做,緣由後面會講到。

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)) {        //這裏的邏輯是若是前節點爲頭結點而且獲取到鎖則進行頭結點變換
                setHead(node);
                p.next = null; // help GC
                failed = false;
                return interrupted;
            }
            if (shouldParkAfterFailedAcquire(p, node) &&    //設置waitStatus狀態
                parkAndCheckInterrupt())                    //掛起線程
                interrupted = true;
        }
    } finally {
        if (failed)
            cancelAcquire(node);                            //取消操做
    }
}

前面的源碼能夠看到它在acquireQueued中對已經入隊的節點進行嘗試鎖的獲取,若是鎖得到就修改頭節點的指針,若是不是頭節點或者爭搶鎖失敗時,此時會進入到shouldParkAfterFailedAcquire方法,這個方法是獲取不到鎖時須要中止繼續無限期等待鎖,其實就是內部的操做邏輯也很簡單,就是若是前節點狀態爲0時,須要將前節點修改成SIGNAL,若是前節點大於0則表明前節點已經被取消了,應該移除隊列,並將前前節點做爲當前節點的前節點,一直循環直到前節點狀態修改成SIGNAL或者前節點被釋放鎖,當前節點獲取到鎖中止循環。

private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
    int ws = pred.waitStatus;
    if (ws == Node.SIGNAL)
        /*
         * 此節點已經設置了狀態,要求對當前節點進行掛起操做
         */
        return true;
    if (ws > 0) {
        /*
         * 若是前節點被取消,則將取消節點移除隊列操做
         */
        do {
            node.prev = pred = pred.prev;
        } while (pred.waitStatus > 0);
        pred.next = node;
    } else {
        /*
         * waitStatus=0或者PROPAGATE時,表示當前節點尚未被掛起中止,須要等待信號來通知節點中止操做。
         */
        compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
    }
    return false;
}

上面的方法其實很容易理解就是等待掛起信號,若是前節點的狀態爲0或PROPAGATE則將前節點修改成SIGNAL,則表明後面前節點釋放鎖後會通知下一個節點,也就是說喚醒下一個能夠喚醒的節點繼續爭搶所資源,若是前節點被取消了那就繼續往前尋找不是被取消的節點,這裏不會找到前節點爲null的狀況,由於它默認會有一個空的頭結點,也就是上圖內容,此時的隊列狀態是如何的咱們看一下,這裏它會進來兩次,覺得咱們上圖能夠看到當前節點前節點是Ref-724此時waitStatus=0,他須要先將狀態更改成SIGNAL也就是運行最有一個else語句,此時又會回到外面的for循環中,因爲方法返回的是false則不會運行parkAndCheckInterrupt方法,而是又循環了一次,此時發現當前節點爭搶鎖又失敗了,而後此時隊列的狀態以下圖所示:

圖片描述

再次進入到方法以後發現前驅節點的waitStatus=-1,表示當前節點須要進行掛起等到,此時返回的結果是true,則會運行parkAndCheckInterrupt方法,這個方法很簡單就是將當前線程進行掛起操做,以下所示:

private final boolean parkAndCheckInterrupt() {
    LockSupport.park(this);                        //掛起線程
    return Thread.interrupted();                //判斷是否被中斷,獲取中斷標識
}

park掛起線程而且響應中斷信息,其實咱們從這裏就能發現一個問題,Thread.interrupted方法是用來獲取是否被中斷的標誌,若是被中斷則返回true,若是沒有被中斷則返回false,噹噹前節點被中斷後,其實就會返回true,返回true這裏並無結束,而是跳到調用地方,也就是acquireQueued方法內部:

if (shouldParkAfterFailedAcquire(p, node) &&
    parkAndCheckInterrupt())
    interrupted = true;

以一個案例來進行分析:

public class ReentrantLockDemo {
    public static void main(String[] args) throws Exception {
        AddDemo runnalbeDemo = new AddDemo();
        Thread thread = new Thread(runnalbeDemo::add);
        thread.start();
        Thread thread1 = new Thread(runnalbeDemo::add);
        thread1.start();
        Thread thread2 = new Thread(runnalbeDemo::add);
        thread2.start();

        Thread.sleep(10000);
        thread1.interrupt();
        System.out.println(runnalbeDemo.getCount());
    }

    private static class AddDemo {
        private final AtomicInteger count = new AtomicInteger();
        private final ReentrantLock reentrantLock = new ReentrantLock();
        private final Condition condition = reentrantLock.newCondition();

        private void add() {
            try {
                reentrantLock.lock();
                count.getAndIncrement();
            } finally {
//                reentrantLock.unlock();
            }
        }

        int getCount() {
            return count.get();
        }
    }
}

經過上面的例子能夠發現,thread1調用中斷方法interrupt(),當調用第一次方法的時候,它會進入到parkAndCheckInterrupt方法,而後線程響應中斷,最後返回true,最後返回到acquireQueued方法內部,整個if語句爲true,則開始設置interrupted=true,僅僅是設置了等於true,可是這離還會進入下一輪的循環,假如說上次的線程沒有完成任務,則沒有獲取到鎖,仍是會進入到shouldParkAfterFailedAcquire因爲已經修改了上一個節點的waitStatus=-1,直接返回true,而後再進入到parkAndCheckInterrupt又被掛起線程,可是若是上步驟操做他正搶到鎖,則會返回ture,外面也會清除中斷標誌位,從這裏能夠清楚地看到acquire方法是一個不間斷得到鎖的操做,可能重複阻塞和解除阻塞操做。

上面阻塞隊列的內容已經講完了,接下來咱們看一下unlock都爲咱們作了什麼工做:

public void unlock() {
    sync.release(1);
}

咱們能夠看到他直接調用了獨佔模式的release方法,看一下具體源碼:

public final boolean release(int arg) {
    if (tryRelease(arg)) {                    //調用ReentrantLock中的Sync裏面的tryRelease方法
        Node h = head;                        //獲取頭節點
        if (h != null && h.waitStatus != 0)    //頭節點不爲空且狀態不爲0時進行unpark方法
            unparkSuccessor(h);                //喚醒下一個未被取消的節點
        return true;
    }
    return false;
}

release方法,首先先進行嘗試去釋放鎖,若是釋放鎖仍然被佔用則直接返回false,若是嘗試釋放鎖時,發現鎖已經釋放,當前線程不在佔用鎖資源時,則會進入的下面進行一些列操做後返回true,接下來咱們先來看一下ReentrantLockSync下的tryRelease方法,以下所示:

protected final boolean tryRelease(int releases) {
    int c = getState() - releases;                                //獲取state狀態,標誌信息減小1
    if (Thread.currentThread() != getExclusiveOwnerThread())    //線程不一致拋出異常
        throw new IllegalMonitorStateException();
    boolean free = false;                                                                                                                        
    if (c == 0) {                                                //是否已經釋放鎖,start=0表明已經釋放鎖                                                
        free = true;                                            //將標誌free設置爲true                                                                                
        setExclusiveOwnerThread(null);                            //取消獨佔鎖信息
    }    
    setState(c);                                                //設置鎖標誌信息                                                                                        
    return free;                                                                                            
}

看上面的源碼,表示首先先獲取state狀態,若是state狀態減小1以後和0不相等則表明有重入鎖,則表示當前線程還在佔用所資源,直到線程釋放鎖返回ture標識,仍是以上例子爲主(此時AddDemo中的unlock不在被註釋),分析其如今的隊列中的狀態

圖片描述

釋放鎖後,進入到if語句中,判斷當前頭節點不爲空且waitStatus!=0,經過上圖也能夠發現頭節點爲-1,則進入到unparkSuccessor方法內:

private void unparkSuccessor(Node node) {
    /*
     * 獲取節點的waitStatus狀態
     */
    int ws = node.waitStatus;
      // 若是小於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);
}

能夠看到它是現將頭節點的狀態更新爲0,而後再喚醒下一個節點,若是下一個節點爲空則直接返回不喚醒任何節點,若是下一個節點被取消了,那麼它會從尾節點往前進行遍歷,遍歷與頭節點最近的沒有被取消的節點進行喚醒操做,在喚醒前看一下隊列狀態:

圖片描述

而後喚醒節點後他會進入到parkAndCheckInterrupt方法裏面,再次去執行下面的方法:

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)) {        //這裏的邏輯是若是前節點爲頭結點而且獲取到鎖則進行頭結點變換
                setHead(node);
                p.next = null; // help GC
                failed = false;
                return interrupted;
            }
            if (shouldParkAfterFailedAcquire(p, node) &&    //設置waitStatus狀態
                parkAndCheckInterrupt())                    //掛起線程
                interrupted = true;
        }
    } finally {
        if (failed)
            cancelAcquire(node);                            //取消操做
    }
}

此時獲取p==head成立,而且能夠正搶到所資源,因此它會進入到循環體內,進行設置頭結點爲當前節點,前節點的下一個節點設置爲null,返回中斷標誌,看一下此時隊列狀況,以下圖所示:

圖片描述

AbstractQueuedSynchronizer的獨佔模式其實提供了三種不一樣的形式進行獲取鎖操做,看一下下表所示:

方法名稱 方法描述 對應調用的內部方法
acquire 以獨佔模式進行不間斷的獲取鎖 tryAcquire,acquireQueued
acquireInterruptibly 以獨佔模式相應中斷的方式獲取鎖,發生中斷拋出異常 tryAcquire,doAcquireInterruptibly
tryAcquireNanos 以獨佔模式相應中斷的方式而且在指定時間內獲取鎖,會阻塞一段時間,若是還未得到鎖直接返回,發生中斷拋出異常 tryAcquire,doAcquireNanos

經過上面圖能夠發現,他都會調用圖表一中須要用戶實現的方法,ReentrantLock實現了獨佔模式則內部實現的是tryAcquiretryRelease方法,用來嘗試獲取鎖和嘗試釋放鎖的操做,其實上面內容咱們用的是ReentrantLock中的lock方法做爲同步器,細心的朋友會發現,這個lock,方法是ReentrantLock實現的,它內部調用了acquire方法,實現了不間斷的獲取鎖機制,ReentrantLock中還有一個lockInterruptibly方法,它內部直接調用的是AbstractQueuedSynchronizeracquireInterruptibly方法,兩個之間的區別在於,二者都會相應中斷信息,前者不會作任何處理還會進入等待狀態,然後者則拋出異常終止操做,

這裏爲了詳細看清楚它內部關係我這裏用張圖來進行闡述,以下所示:

圖片描述

  1. 左側表明的事ReentrantLock,右側表明的AQS
  2. 左側內部黃色區域表明NonfairSync
  3. 圖中1和2表明AQS調用其餘方法的過程

接下來咱們來看一下源碼信息:

public void lockInterruptibly() throws InterruptedException {
    sync.acquireInterruptibly(1);
}

發現他調用的Sync類中的acquireInterruptibly方法,但其實這個方法是AQS中的方法,源碼以下所示:

public final void acquireInterruptibly(int arg)
        throws InterruptedException {
    if (Thread.interrupted())                                                //判斷線程是否被中斷
        throw new InterruptedException();                        //中斷則拋出異常
    if (!tryAcquire(arg))                                                        //嘗試獲取鎖
        doAcquireInterruptibly(arg);                                //進行添加隊列,而且修改前置節點狀態,且響應中斷拋出異常
}

經過上面的源碼,它也調用了子類實現的tryAcquire方法,這個方法和咱們上文提到的tryAcquire是同樣,ReentrantLock下的NonfairSync下的tryAcquire方法,這裏這個方法就很少說了詳細請看上文內容,這裏主要講一下doAcquireInterruptibly這個方法:

private void doAcquireInterruptibly(int arg)
    throws InterruptedException {
    final Node node = addWaiter(Node.EXCLUSIVE);        //將節點添加到隊列尾部
    boolean failed = true;                                //失敗匹配機制
    try {
        for (;;) {
            final Node p = node.predecessor();            //獲取前節點
            if (p == head && tryAcquire(arg)) {            //若是前節點爲頭節點而且得到了鎖
                setHead(node);                            //設置當前節點爲頭節點
                p.next = null; // help GC                //頭節點的下一個節點設置爲null
                failed = false;                            //匹配失敗變爲false
                return;
            }        
            if (shouldParkAfterFailedAcquire(p, node) &&    //將前節點設置爲-1,若是前節點爲取消節點則往前一直尋找直到修改成-1爲止。
                parkAndCheckInterrupt())                    //掛起線程返回是否中斷
                throw new InterruptedException();
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

其實這個方法和acquireQueued區別在於如下幾點:

  1. acquireQueued是在方法內部添加節點到隊列尾部,而doAcquireInterruptibly是在方法內部進行添加節點到尾部,這個區別點並非很重要
  2. 重點是acquireQueued響應中斷,可是他不會拋出異常,然後者會拋出異常throw new InterruptedException()

分析到這裏咱們來用前面的例子來進行模擬一下中中斷的操做,詳細代碼以下所示:

public class ReentrantLockDemo {

    public static void main(String[] args) throws Exception {
        AddDemo runnalbeDemo = new AddDemo();
        Thread thread = new Thread(runnalbeDemo::add);
        thread.start();
        Thread.sleep(500);
        Thread thread1 = new Thread(runnalbeDemo::add);
        thread1.start();
        Thread.sleep(500);
        Thread thread2 = new Thread(runnalbeDemo::add);
        thread2.start();
        Thread.sleep(500);
        Thread thread3 = new Thread(runnalbeDemo::add);
        thread3.start();
        Thread.sleep(10000);
        thread1.interrupt();
        System.out.println(runnalbeDemo.getCount());
    }

    private static class AddDemo {
        private final AtomicInteger count = new AtomicInteger();
        private final ReentrantLock reentrantLock = new ReentrantLock();
        private final Condition condition = reentrantLock.newCondition();

        private void add() {
            try {
                reentrantLock.lockInterruptibly();
                count.getAndIncrement();
            } catch (Exception ex) {
                System.out.println("線程被中斷了");
            } finally {
//                reentrantLock.unlock();
            }
        }

        int getCount() {
            return count.get();
        }
    }
}

上面的例子其實和前面提到的例子沒有什麼太大的差異主要的差異是將lock替換爲lockInterruptibly,其次就是在三個線程後面講線程1進行中斷操做,這裏入隊的操做不在多說,由於操做內容和上面大體相同,下面是四個個線程操做完成的狀態信息:

圖片描述
若是線程等待的過程當中拋出異常,則當前線程進入到finally中的時候failed爲true,由於修改該字段只有獲取到鎖的時候纔會修改成false,進來以後它會運行cancelAcquire來進行取消當前節點,下面咱們先來分析下源碼內容:

private void cancelAcquire(Node node) {
    // 若是節點爲空直接返回,節點不存在直接返回
    if (node == null)
        return;
        // 設置節點所在的線程爲空,清除線程操做
    node.thread = null;

    // 獲取當前節點的前節點
    Node pred = node.prev;
      // 若是前節點是取消節點則跳過前節點,一直尋找一個不是取消節點爲止
    while (pred.waitStatus > 0)
        node.prev = pred = pred.prev;

       // 獲取頭節點下一個節點
    Node predNext = pred.next;

    // 這裏直接設置爲取消節點狀態,沒有使用CAS緣由是由於直接設置只有其餘線程能夠跳過取消的節點
    node.waitStatus = Node.CANCELLED;

    // 若是當前節點爲尾節點,而且設置尾節點爲找到的合適的前節點時,修改前節點的下一個節點爲null
    if (node == tail && compareAndSetTail(node, pred)) {
        compareAndSetNext(pred, predNext, null);
    } else {
        // 若是不是尾節點,則說明是中間節點,則須要通知後續節點,嘿,夥計你被喚醒了。
        int ws;
        if (pred != head &&                                                            //前節點不是頭結點
            ((ws = pred.waitStatus) == Node.SIGNAL ||        // 前節點的狀態爲SIGNAL 
             (ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) //或者前節點狀態小於0並且修改前節點狀態爲SIGNAL成功 
               && pred.thread != null) {                                            //前節點線程不爲空
            Node next = node.next;
            if (next != null && next.waitStatus <= 0)
                compareAndSetNext(pred, predNext, next);
        } else {
              //喚醒下一個不是取消的節點
            unparkSuccessor(node);
        }

        node.next = node; // help GC
    }
}
  1. 首先找到當前節點的前節點,若是前節點爲取消節點則一直往前尋找一個節點。
  2. 取消的是尾節點,則直接將前節點的下一個節點設置爲null
  3. 若是取消的是頭節點的下一個節點,且不是尾節點的狀況時,它是喚醒下一個節點,喚醒以前並無將其移除隊列,而是在喚醒下一個節點的時候,shouldParkAfterFailedAcquire裏面將取消的節點移除隊列,喚醒以後,當前節點的下一個節點也設置成本身,幫助GC回收它。
  4. 若是取消節點是中間的節點,則直接將其前節點的下一個節點設置爲取消節點的下下個節點便可。

第一種狀況若是咱們取消的節點是前節點是頭節點,此時線程1的節點應該是被中斷操做,此時進入到cancelAcquire以後會進入else語句中,而後進去到unparkSuccessor方法,當進入到這個方法以前咱們看一下狀態變化:

圖片描述

咱們發現線程1的Node節點的waitStatus變爲1也就是Node.CANCELLED節點,而後運行unparkSuccessor方法,該方法上面就已經講述了其中的源碼,這裏就不在貼源碼了,就是要喚醒下一個沒有被取消的節點,這裏是Ref-695這個線程,當Ref-695被喚醒以後它會繼續運行下面的內容:

private void doAcquireInterruptibly(int arg)
    throws InterruptedException {
    final Node node = addWaiter(Node.EXCLUSIVE);
    boolean failed = true;
    try {
        for (;;) {
            final Node p = node.predecessor();
            if (p == head && tryAcquire(arg)) {                //再一次循環發現仍是沒有爭搶到鎖
                setHead(node);
                p.next = null; // help GC
                failed = false;
                return;
            }
            if (shouldParkAfterFailedAcquire(p, node) &&    //再一次循環以後有運行到這裏了
                parkAndCheckInterrupt())                    //這裏被喚醒了,又要進行循環操做了
                throw new InterruptedException();
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

發現再一次循環操做後,仍是沒有正搶到鎖,這時候仍是會運行shouldParkAfterFailedAcquire方法,這個方法內部發現前節點的狀態是Node.CANCELLED這時候它會在內部先將節點給幹掉,也就是這個代碼:

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;
}

最後仍是會被掛起狀態,由於沒有釋放鎖操做,最後移除的節點以下所示:

圖片描述

若是取消的事尾節點,也就是線程3被中斷操做,這個是比較簡單的直接將尾節點刪除便可,其中會走以下代碼:

if (node == tail && compareAndSetTail(node, pred)) {
    compareAndSetNext(pred, predNext, null);
}

圖片描述

若是取消的節點是中間的節點,經過上例子中則是取消線程2,其實它內部只是將線程取消線程的前節點的下一個節點指向了取消節點的下節點,以下圖所示:

圖片描述

結束語

這章節分析的主要是ReentrantLock的內部原理,原本公平模式和非公平模式想放在一塊兒來寫,無奈發現篇幅有點長了,因此就分開進行寫,這樣讀取來不會那麼費勁,內部還有條件內容等待下章節分析,若是有分析不到位的請你們指正。

相關文章
相關標籤/搜索