AQS互斥模式源碼解析

AbstractQueuedSynchronizer

我們知道,Java中很多重要的併發組件都是基於AQS進行設計的,AQS本身是一個類,但是不如說他是一個框架,該框架爲衆多併發組件提供了底層基礎。本篇文章致力於分析AQS的源碼,以瞭解AQS的執行機制。在討論AQS的具體邏輯之前,首先我們討論AQS的父類AbstractOwnableSynchronizer。

AbstractOwnableSynchronizer

AQS,全稱就是AbstractQueuedSynchronizer,在java.util.concurrent.locks包中,下面是它的類繼承結構圖:

在這裏插入圖片描述

該類的類繼承結構極其簡單,AbstractQueuedSynchronizer僅僅繼承了AbstractOwnableSynchronizer。我們可以先查看AbstractOwnableSynchronizer的功能以及源碼。該類代碼如下:

public abstract class AbstractOwnableSynchronizer
    implements java.io.Serializable {
    
    private static final long serialVersionUID = 3737899427754241961L;
    
    protected AbstractOwnableSynchronizer() { }

    private transient Thread exclusiveOwnerThread;

    protected final void setExclusiveOwnerThread(Thread thread) {
        exclusiveOwnerThread = thread;
    }

    protected final Thread getExclusiveOwnerThread() {
        return exclusiveOwnerThread;
    }
}

可以看到AbstractOwnableSynchronizer是一個只有一個屬性的抽象類,其屬性只有exclusiveOwnerThread

private transient Thread exclusiveOwnerThread;

並且也只有兩個方法,即exclusiveOwnerThread屬性的getter和setter方法。而且我們甚至不能對其中的方法進行任何覆蓋操作,那它用來做什麼呢?

查看AbstractOwnableSynchronizer的註釋,我們可以發現,該類是一個同步器,用於保存獨佔模式下,持有資源的線程。

/**
 * The current owner of exclusive mode synchronization.
 */
private transient Thread exclusiveOwnerThread;

我們查看exclusiveOwnerThread的屬性名,也可以發現,該屬性名字的含義是專有的擁有者線程。對於一個抽象的同步器來說,爲什麼要繼承如此一個超類呢?

我們考察一個分佈式鎖的邏輯。A資源不允許兩個系統B、C同時修改,現在我們要加一個分佈式鎖來限制這件事。怎麼處理呢?在B系統準備修改A資源之前,先檢查一下Redis,是否有一個叫做A_Lock的key存在,如果有,則等到這個key被刪除之後再進行下一步,如果沒有,先在Redis中添加一個key名爲A_Lock的記錄,然後再操作A資源,操作結束後,刪除key爲A_Lock的記錄。C系統同理。

上面的情況非常常見的,而這個key爲A_Lock的記錄就是我們這裏AbstractQueuedSynchronizerexclusiveOwnerThread屬性。我們可以查看一下setExclusiveOwnerThread(Thread thread)方法的調用方,可以發現都是如下兩個方法:

  1. tryAcquire(int unused)方法:都是setExclusiveOwnerThread(current);
  2. tryRelease(int unused)方法:都是setExclusiveOwnerThread(null);

這兩個方法通過方法名我們就知道是用來加鎖、釋放鎖的,可以看到,加鎖時將線程放入到exclusiveOwnerThread中,解鎖時,則放null進去。

至此,我們就瞭解了AbstractOwnableSynchronizer的作用。下面讓我們專心分析AbstractQueuedSynchronizer

AbstractQueuedSynchronizer

AbstractQueuedSynchronizer基於AbstractOwnableSynchronizer完成了自己的工作,那麼AbstractQueuedSynchronizer到底完成了什麼工作呢?查看註釋我們可以看到:

/**
 * This class is designed to
 * be a useful basis for most kinds of synchronizers that rely on a
 * single atomic {@code int} value to represent state.
 * /

上面註釋說到,該類是大多數依賴單個原子性int值表示同步狀態的同步器的基礎。那麼該類到底是怎麼實現該功能的呢?註釋的第一句話就有說明:

/**
 * Provides a framework for implementing blocking locks and
 *  related synchronizers (semaphores, events, etc) that rely on
 *  first-in-first-out (FIFO) wait queues.
 * /

可以看到,該類實現上述功能依賴了FIFO的阻塞隊列。但是,Java中的阻塞隊列不應該是基於各種鎖,同步器麼?這樣不就出現了循環依賴麼?那麼AbstractQueuedSynchronizer是使用的怎樣的阻塞隊列呢?我們考察該類的屬性,可以看到該類與隊列相關的屬性很少,只有兩個:

private transient volatile Node head;
private transient volatile Node tail;

顯而易見,這就是阻塞隊列的頭結點和尾節點,鏈表本身就用於存儲數據,讓我們考察一下該類的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;
    static final int PROPAGATE = -3;

    volatile int waitStatus;
    volatile Node prev;
    volatile Node next;
    volatile Thread thread;
    Node nextWaiter;
}

可以看到Node節點有4個屬性,分別是:

  1. waitStatus:表示當前節點包含線程的狀態。該狀態有5種:
    1. CANCELLED:這意味着該線程已經因爲超時或者中斷而取消了,這是一個終結態。
    2. SIGNAL:表示當前線程的後繼線程被阻塞或者即將被阻塞,當前線程釋放鎖或者取消後需要喚醒後繼線程,注意這個狀態一般都是後繼線程來設置前驅節點的。
    3. CONDITION:說明當前線程在CONDITION隊列
    4. PROPAGATE:用於將喚醒後繼線程傳遞下去,這個狀態的引入是爲了完善和增強共享鎖的喚醒機制。在一個節點成爲頭節點之前,是不會躍遷爲此狀態的
    5. 0:表示無狀態
  2. prev:前驅節點
  3. next:後繼結點
  4. thread:該節點表示的線程
  5. nextWaiter:SHARED or EXCLUSIVE,該屬性描述鎖的類型,而且需要注意,用來描述鎖類型的是一個Node節點,而不是一個int值。

Node類的註釋詳細解釋了該隊列的運行邏輯,該隊列名爲CLH隊列,是以三個人的名字命名的。但是由於註釋實在是太長了。這裏筆者對其簡化,這裏介紹一下。

CLH隊列+自旋鎖是構造一個同步器的基礎方案,AQS就是採用了這種方法,對於CLH隊列,其最重要的特點就是當前節點根據當前節點的前驅節點判斷前一個線程是否釋放了鎖。這一點十分重要。除此之外該隊列與普通的雙向隊列區別不大。接下來我們會根據AQS的源碼分析CLH隊列和CAS到底是怎麼完成同步器的功能的。

AQS中加鎖、解鎖的方法有很多,例如:

  1. acquire(int arg):獲取互斥鎖
  2. acquireInterruptibly(int arg):獲取互斥鎖,外界可以對加鎖過程進行中斷
  3. acquireShared(int arg):獲取共享鎖
  4. acquireSharedInterruptibly(int arg):獲取共享鎖,外界可以對加鎖過程進行中斷
  5. release(int arg):釋放互斥鎖
  6. releaseShared(int arg):釋放共享鎖

讓我們首先分析acquire(int arg)release(int arg)方法,即互斥鎖的加鎖與解鎖流程,因爲這兩個方法相對最簡單,也最能讓人理解CLH隊列的功能。

1. acquire(int arg)

acquire(int arg)方法用於獲取互斥鎖,讓我們考察該方法源碼:

public final void acquire(int arg) {
    // 嘗試獲取鎖
    // 如果沒有獲取到,則創建一個節點放入隊列中
    // 等待獲取到鎖,如果獲取過程中,出現特殊情況,
    // 則中斷當前線程,避免阻塞
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}

tryAquire(int)方法負責嘗試獲取鎖,但是AQS實際上並沒有對這個方法的具體實現,而子類可以覆蓋該方法,也正因爲如此,纔有了ReentrantLock,這一系列的同步器。

這裏我們只需要知道在使用AQS獲取互斥鎖時,都是先調用tryAquire(int)嘗試獲取鎖的,如果獲取不到,就會將當前線程的信息存儲到我們之前說的CLH隊列中,至於存儲的過程,讓我們考察一下addWaiter(Node mode)方法,源碼如下:

   private Node addWaiter(Node mode) {
        // 創建一個Node,並通過傳入參數設計其加鎖模式
        // 監聽的線程就是當前線程
        Node node = new Node(Thread.currentThread(), mode);
        // 獲取隊尾節點
        // 如果隊列不爲空,那麼將當前節點與隊尾節點連接
        // 然後執行CAS操作,將當前節點添加到pred後面自旋鎖
        Node pred = tail;
        if (pred != null) {
            node.prev = pred;
            if (compareAndSetTail(pred, node)) {
                pred.next = node;
                return node;
            }
        }
        // 如果隊列是空,那麼就創建一個空節點作爲隊列頭,然後將新節點插入
        enq(node);
        return node;
    }

可以看到,該方法傳入了一個Node節點,注意這個Node節點並不是我們要創建的節點,它只是一個標識,標識要創建的節點標識互斥還是同步真正的線程數據其實根本不需要通過參數傳輸,因爲現在本身就是在這個線程中

接下來讓我們查看一下enq(final Node)中的細節,該方法用於處理隊列是空,創建一個空節點作爲隊列頭,然後將新節點插入的情況,源碼如下:

	private Node enq(final Node node) {
        // 自旋+CAS 將Node添加到隊列中
        for (;;) {
            Node t = tail;
            // 如果隊列爲空
            // 這時創建一個New Node作爲head和tail
            if (t == null) { // Must initialize
                if (compareAndSetHead(new Node()))
                    tail = head;
            // 將新節點插入到對列尾部
            } else {
                node.prev = t;
                if (compareAndSetTail(t, node)) {
                    t.next = node;
                    return t;
                }
            }
        }
    }

這裏有一個細節需要注意,如果隊列爲空,會創建一個空的Node最爲隊列頭,然後再將要添加的Node加入到隊列尾部。其中新構建的隊列頭節點,waitStatus=0。這裏就開始暴露了CLH隊列的特點,通過前驅節點判斷前一個線程是否釋放鎖。接下來我們繼續向下看:

將線程信息放入到隊列中後,就要開始獲取鎖了,acquireQueued方法用於持續的獲取鎖。在使用鎖的時候,我們會看到,如果一個線程未獲取到鎖,會一直阻塞,直到獲取到鎖爲止,該方法就是造成上述現象的主要原因,源碼如下:

	final boolean acquireQueued(final Node node, int arg) {
        boolean failed = true;
        try {
            boolean interrupted = false;
            // 自旋+獲取鎖
            for (;;) {
                final Node p = node.predecessor();
                // 獲取node的前一個節點,查看是否是頭結點
                // 如果是頭結點再進行獲取鎖
                // 如果獲取到了,就將隊列頭設置爲它
                if (p == head && tryAcquire(arg)) {
                    setHead(node);
                    p.next = null; // help GC
                    failed = false;
                    return interrupted;
                }
                // 在沒有成功獲取鎖的情況下判斷是否應該中斷獲取鎖
                // 判斷邏輯在`shouldParkAfterFailedAcquire`方法中
                // 掛起當前線程,避免佔用資源。
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            // 如果沒有成功獲取鎖,那麼要喚醒線程,並進行一系列的處理
            if (failed)
                cancelAcquire(node);
        }
    }

這裏我們看到,acquireQueued(final Node node, int arg)方法會通過自旋的方式不斷檢測當前節點的前驅節點是否是頭結點,如果是,則嘗試獲取鎖。如果獲取到了鎖,就將其設置爲頭結點。這就意味着,能夠有機會競爭鎖的永遠是CLH隊列的頭結點的下一個節點,而頭結點代表的線程是真正擁有鎖的線程。而且有一個小細節,如果某個線程沒有獲取到鎖,難道要一直不斷進行自旋,然後CAS競爭鎖麼?那如果有20個線程在等待鎖,資源豈不是很大的浪費。我們考察shouldParkAfterFailedAcquire(p, node)方法,通過方法名我們知道,如果競爭鎖失敗,那麼線程就會被掛起。考察源碼如下:

    private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
        int ws = pred.waitStatus;
        if (ws == Node.SIGNAL)
            // 如果當前節點的前驅節點的狀態是`SIGNAL`,
            // 這意味着前驅節點線程佔用的鎖釋放之後,就會分配給當前節點的線程
            // 因此掛起當前線程就可以了,直接返回true
            return true;
        if (ws > 0) {
            // 如果前驅節點的狀態是`CANCEL`,這意味着前驅節點的任務已經被取消了,
            // 這意味着要使用第一個非`CANCEL`狀態的節點與當前節點進行對比
            do {
                node.prev = pred = pred.prev;
            } while (pred.waitStatus > 0);
            // 修改指針
            pred.next = node;
        } else {
            // 到達這裏證明前驅節點的狀態是0或者PROPAGATE
            // 那麼這時候需要前驅節點線程釋放鎖之後通知當前節點線程
            // 因此需要修改前驅節點線程狀態爲Node.SIGNAL
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
        }
        return false;
    }

我們知道,AQS中Node的狀態有5個,分別表示:

  1. CANCELLED:這意味着該線程已經因爲超時或者中斷而取消了,這是一個終結態。
  2. SIGNAL:表示當前線程的後繼線程被阻塞或者即將被阻塞,當前線程釋放鎖或者取消後需要喚醒後繼線程,注意這個狀態一般都是後繼線程來設置前驅節點的。
  3. CONDITION:說明當前線程在CONDITION隊列
  4. PROPAGATE:用於將喚醒後繼線程傳遞下去,這個狀態的引入是爲了完善和增強共享鎖的喚醒機制。在一個節點成爲頭節點之前,是不會躍遷爲此狀態的
  5. 0:表示無狀態

這裏我們不考慮CONDITIONPROPAGATE,這兩個狀態與互斥模式關係不大。主要考慮剩下三個狀態。我們已經知道,CLH隊列規則是通過前驅節點狀態判斷前一個線程是否釋放鎖,接下來考慮三種狀態:

  1. SIGNAL:如果前一個線程的狀態是SINGAL,這說明,前一個線程釋放鎖會通知後一個線程,也就是當前線程獲取鎖。那樣當前線程直接掛起等着就好了。
  2. CANCELLED:如果前一個線程被取消了,不獲取鎖了,那麼就應該與當前節點的前一個非CANCELLED的節點比較再進行決定,因此這裏先不進行掛起操作,更新一下節點,然後再去嘗試獲取一次鎖,如果獲取不到,再該掛起掛起。
  3. 0:如果前驅節點的狀態爲0,這個就是我們之前說過的,隊列中的第一個節點嘛,我們都知道這個節點就是個標誌性的節點,直接將他設置爲SIGNAL,然後再次嘗試獲取鎖,如果獲取到了,那就最好,獲取不到,在前一線程喚醒後也會通知當前線程。

如果shouldParkAfterFailedAcquire(Node,Node)返回true,那麼意味着當前Node已經加入到阻塞隊列中了,因此需要掛起當前線程,直到當前節點的前驅節點釋放鎖爲止。parkAndCheckInterrupt()方法就負責掛起當前線程:

    private final boolean parkAndCheckInterrupt() {
        // 掛起當前線程
        LockSupport.park(this);
        return Thread.interrupted();
    }

此時線程就被掛起了,知道有其他線程喚醒該線程爲止。上面就介紹完了獲取互斥鎖成功的全過程,下面給出圖示,方便理解AQS在多個線程獲取鎖時CLH隊列是如何變化的。

我們假設有ABC三個線程同時獲取鎖,我們假設,他們調用acquire(int)的時序是A->B->C,並且規定A線程會持續持有鎖,直到C線程獲取完畢鎖之後纔會釋放鎖。爲了達到這種狀態,使用瞭如下代碼:

public class TestThread extends Thread{

    Lock lock;
    public TestThread(Lock lock) {
        this.lock = lock;
    }
    @Override
    public void run() {
        lock.lock();
        System.out.println(this.getName()+" lock");
    }
}

public class Main {

    public static void main(String[] args) {
        ReentrantLock reentrantLock = new ReentrantLock(false);
        new TestThread(reentrantLock).start();
        new TestThread(reentrantLock).start();
        new TestThread(reentrantLock).start();
    }
}

雖然實現方式比較暴力,但是這是我能想到的,最簡單而且沒有其他併發包組件影響的實現方式了,就先使用該方式實現吧。讓我們考慮加鎖流程:

  1. A線程由於是第一個獲取鎖的,因此,直接獲取到了鎖。並沒有往AQS的CLH隊列中添加節點。所以此時,AQS隊列中是空的。

  2. B線程是第二個獲取鎖的,此時,由於A線程在持有鎖,所以tryAquire(int)方法執行失敗。此時往CLH隊列中添加節點,將B線程信息放入到CLH隊列中。由於當前隊列是空的,所以需要添加兩個節點,隊列頭和保存B線程數據的節點。在執行完addWaiter(Node)方法後,CLH隊列狀態如下:

    在這裏插入圖片描述

  3. 將B線程的節點放入到CLH隊列中後,需要進行CAS+自旋獲取鎖,也就是執行acquireQueued(Node,int)方法。acquireQueued(Node,int)方法會嘗試獲取一次鎖,此時由於A線程還未釋放鎖,因此,獲取失敗,此時,CLH隊列仍然是上面的狀態。由於獲取失敗,會嘗試判斷B線程是否要掛起,則調用shouldParkAfterFailedAcquire(Node,Node)方法。此時,由於保存B信息的前一個節點的waitStatus=0,因此,會將其從0改爲SIGNAL。然後返回false,再次嘗試獲取鎖。此時狀態如下:

    在這裏插入圖片描述

  4. 經過第三步後,B線程再次嘗試獲取鎖,此時,由於A線程仍然沒有釋放鎖,因此,再次調用shouldParkAfterFailedAcquire(Node,Node)方法,返回true,表示B線程可以被掛起,所以掛起B線程。

  5. 此時C線程開始獲取鎖,同樣由於A線程持有鎖,所以,調用`addWaiter(Node)方法,向CLH中添加一個節點。調用之後,CLH隊列狀態如下:

    在這裏插入圖片描述

  6. 將C線程信息添加到CLH隊列中後,開始調用acquireQueued(Node,int)嘗試獲取鎖,這次同樣獲取不到鎖,因爲A線程仍然持有鎖,因此,會調用shouldParkAfterFailedAcquire(Node,Node)方法判斷是否要掛起線程,因爲B線程節點waitStatus=0,所以shouldParkAfterFailedAcquire(Node,Node)方法返回false,並將B線程節點的waitStatus設置爲SIGNAL,CLH隊列狀態如下:

    在這裏插入圖片描述

  7. 最後C線程再次獲取一次鎖,發現仍然獲取不到,這時候再判斷掛起,發現應該掛起,此時就掛起C線程。然後結束了。

掛起的線程會在持有鎖的線程釋放鎖之後被喚醒,這部分會在release(int)方法中體現。當掛起的線程被喚醒之後,由於循環的原因會再次嘗試獲取鎖。

這裏給出正常加鎖時的流程圖:

在這裏插入圖片描述

前面討論和演示的是正常加鎖的邏輯,事實上,有一部分的線程的加鎖過程會被強制中斷,或者由於某些異常而無法正常獲取到鎖,這時AQS爲了讓其他的線程正常獲取鎖,就會對這類無法獲取到鎖的節點進行處理,處理工作由cancelAcquire(Node node)方法完成。我們首先查看何時會進行這個處理獲取鎖失敗的節點的操作,再次查看acquireQueue(Node,int)方法源碼:

```java
	final boolean acquireQueued(final Node node, int arg) {
        boolean failed = true;
        try {
            boolean interrupted = false;
            // 自旋+獲取鎖
            for (;;) {
                final Node p = node.predecessor();
                // 獲取node的前一個節點,查看是否是頭結點
                // 如果是頭結點再進行獲取鎖
                // 如果獲取到了,就將隊列頭設置爲它
                if (p == head && tryAcquire(arg)) {
                    setHead(node);
                    p.next = null; // help GC
                    failed = false;
                    return interrupted;
                }
                // 在沒有成功獲取鎖的情況下判斷是否應該中斷獲取鎖
                // 判斷邏輯在`shouldParkAfterFailedAcquire`方法中
                // 掛起當前線程,避免佔用資源。
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            // 如果沒有成功獲取鎖,那麼要喚醒線程,並進行一系列的處理
            if (failed)
                cancelAcquire(node);
        }
    }

我們注意,這裏只有在false爲true,並且循環被停止的情況下,會執行cancelAcquire(node);能達到這個目的的只有拋出異常。那麼上述代碼哪裏會拋出異常呢?

  1. final Node p = node.predecessor();會拋出空指針異常:

    final Node predecessor() throws NullPointerException {
            Node p = prev;
            if (p == null)
                throw new NullPointerException();
            else
                return p;
    }
    

    如果當前節點的前驅節點是null,那麼會拋出空指針異常。不過一般不會出現這種情況,因爲AQS的設計十分嚴謹。

  2. tryAcquire(arg)該方法是使用AQS的客戶端實現的,可能會拋出RuntimeException

  3. parkAndCheckInterrupt()該方法負責將線程掛起,但是有些情況線程會拋出InterruptedException

當然上面只列出了筆者能想到的情況,可能還有別的部分會導致循環中斷。但是無論出現哪種情況,都會由cancelAcquire(node);方法進行處理,下面我們查看cancelAcquire(node);方法源碼:

    private void cancelAcquire(Node node) {
        if (node == null)
            return;
        // 清除Node數據
        node.thread = null;
       	// 如果該Node的前驅節點都是被取消的任務
    	// 那麼將該Node添加到最後一個可用任務之後
        Node pred = node.prev;
        while (pred.waitStatus > 0)
            node.prev = pred = pred.prev;
        // 獲取當前節點的前驅節點的下一個節點,用於進行後面的CAS操作
        Node predNext = pred.next;

        // 將獲取鎖不成功的節點設置爲CANCELLED,避免影響其他節點工作
        node.waitStatus = Node.CANCELLED;

        // 如果當前節點是尾部節點,那麼就使用CAS將尾部節點設置爲前一個節點,目的是爲了清除當前節點
        // 情況1
        if (node == tail && compareAndSetTail(node, pred)) {
            compareAndSetNext(pred, predNext, null);
        } else {
            // 由於有些情況下,隊列中有的節點的狀態是SIGNAL,因此這類節點被刪除時,需要將其前一個可用節點的狀態設置爲SIGNAL
            // 該部分代碼就用於處理該問題
            // 情況2
            int ws;
            if (pred != head &&
                ((ws = pred.waitStatus) == Node.SIGNAL ||
                 (ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) &&
                pred.thread != null) {
                Node next = node.next;
                if (next != null && next.waitStatus <= 0)
                    compareAndSetNext(pred, predNext, next);
            } else {
            // 情況3
                unparkSuccessor(node);
            }
            node.next = node; // help GC
        }
    }

首先介紹AQS處理獲取鎖失敗的節點的統一步驟,即如下代碼:

		if (node == null)
            return;
        // 清除Node數據
        node.thread = null;
       	// 如果該Node的前驅節點都是被取消的任務
    	// 那麼將該Node添加到最後一個可用任務之後
        Node pred = node.prev;
        while (pred.waitStatus > 0)
            node.prev = pred = pred.prev;
        // 獲取當前節點的前驅節點的下一個節點,用於進行後面的CAS操作
        Node predNext = pred.next;

        // 將獲取鎖不成功的節點設置爲CANCELLED,避免影響其他節點工作
        node.waitStatus = Node.CANCELLED;

當AQS認定該節點是獲取鎖失敗的節點後,會統一先將節點的thread設置爲null,然後通過鏈表操作,將獲取鎖失敗的節點之前所有的CANCELLED的節點全部刪除。最後將節點狀態設置爲CANCELLED。這裏其實帶來了一些問題,這個問題我們到下一節再提。

處理獲取鎖失敗的節點(爲了方便描述,接下來統一將這類節點都稱爲失敗節點)時分爲三種情況:

  1. 失敗節點是CLH隊尾節點

    處理這部分邏輯的代碼如下:

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

    處理邏輯就是將錯誤節點的前一個節點設置爲隊尾,然後將前一節點的下一節點設置爲null。注意這裏沒有使用CAS+自旋的方式。因爲事實上隊尾節點的下一節點是否爲null並不重要,因爲addWaiter(Node)創建新的隊尾節點時,並沒有對tail.next進行驗證,所以這裏就算沒有設置成功也沒有什麼關係。處理過程如下圖所示:

    在這裏插入圖片描述

  2. 失敗節點的前驅節點不是隊列頭節點、其狀態不是CANCELLED,而且其代表的線程不爲null。
    處理這部分邏輯的代碼如下:

    		int ws;
            if (pred != head &&
                ((ws = pred.waitStatus) == Node.SIGNAL ||
                 (ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) &&
                pred.thread != null) {
                Node next = node.next;
                if (next != null && next.waitStatus <= 0)
                    compareAndSetNext(pred, predNext, next);
            }
    

    由於此種情況下,失敗節點應該在CLH的中間位置,而且其前驅節點在我們討論的互斥鎖的情況下理論上應該是SIGNAL狀態,除非其前驅節點獲取鎖也失敗了。這時如果後繼節點的waitStatus<=0,就將前驅節點的下一個節點設置爲後繼結點。處理過程如下:

    在這裏插入圖片描述
    這裏可以看到一個特別的情況,next的前驅節點仍然是node,我們都知道在調用acquireQueue(Node,int)嘗試獲取鎖時會判斷當前節點的前驅節點是否爲head。如果出現這種情況,那豈不是永遠都不會讓next的前驅節點變爲head?實際上並不是這樣的。如果節點獲取不到鎖會進入shouldParkAfterFailedAcquire(Node pred, Node node)方法,該方法並不僅僅只是判斷該線程是否應該park,而且還將當前節點之前所有的CANCELLED狀態的節點消除了。所以就不會有這個問題了。當然如果next節點也獲取鎖失敗了,那也可能是cancelAcquire(Node node)方法處理這個問題。

  3. 失敗節點的前驅節點是頭結點,或者失敗節點的前驅節點狀態是CANCELLED或者最後失敗節點的前驅節點代表的thread=null。
    需要注意,上面的設置、判斷等一系列操作,都是通過一次CAS進行操作或者判斷的,因此可能在處理到一半就中斷了,例如第二步,可能之前的很久時間失敗節點的前驅節點是SIGNAL狀態的,但是在判斷的那一刻突然變成了-1。那麼此時都要交給第三種情況處理。這也就意味着第三種情況的處理方案應該是最通用的。讓我們來查看該情況的處理方案:

    unparkSuccessor(node);
    

    該方法負責喚醒node節點代表的線程的下一個線程,那麼爲什麼說它是通用的呢?考慮unparkSuccessor(Node node)方法源碼:

    private void unparkSuccessor(Node node) {
        int ws = node.waitStatus;
        // 如果節點狀態小於0,那麼將節點狀態設置爲0
        // 這樣會使後繼節點再次獲取一次鎖
        // 因爲如果前驅節點waitStatus爲0,會在獲取鎖失敗後將前驅節點的waitStatus設置爲SIGNAL後再次進行獲取鎖
        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);
    }
    

    可以看到該方法總共做了兩件事:

    1. 將當前節點waitStatus設置爲0,使得後繼節點能夠再次獲取一次鎖
    2. 查找下一個要喚醒的線程,然後將其進行喚醒,查找邏輯有兩種:
      1. 查看當前節點的後繼節點是否是null,並且waitStatus <=0。 如果是的話,就激活該節點。
      2. 否則從隊列尾部向前查找,找到最後一個waitStatus<=0的節點,喚醒它。

至此aquire(int)方法邏輯分析完畢,我們也瞭解了AQS到底如何利用CLH隊列的。

2. release(int arg)

release(int arg)方法是aquire(int)的逆過程,主要用於釋放獨佔鎖,相比於aquire(int)來說,release(int arg)的邏輯相對簡單的多:

public final boolean release(int arg) {
    // 嘗試釋放鎖
    if (tryRelease(arg)) {
        // 如果釋放成功則嘗試喚醒下一個等待鎖的線程
        Node h = head;
        if (h != null && h.waitStatus != 0)
            unparkSuccessor(h);
        return true;
    }
    return false;
}

可以看到,該方法僅僅是調用一次tryRelease(int)方法,然後,根據頭結點信息,修改頭結點業務信息,判斷是否要觸發後面線程獲取鎖,即調用unparkSuccessor(Node)方法,該方法的源碼上面已經介紹過,這裏不再贅述。

最後同樣給出release(int)方法的流程圖,方便理解。

在這裏插入圖片描述

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