AQS源碼一窺-JUC系列

AQS源碼一窺

考慮到AQS的代碼量較大,涉及信息量也較多,計劃是先使用較常用的ReentrantLock使用代碼對AQS源碼進行一個分析,一窺內部實現,然後再全面分析完AQS,最後把以它爲基礎的同步器都解析一遍。

暫且可以理解AQS的核心是兩部分組成:

  • volatile修飾的int字段state,表示同步器狀態
  • FIFO同步隊列,隊列是由Node組成

節點模式

Node定義中包含的字段,意味着節點擁有模式的屬性。

  • 獨佔模式(EXCLUSIVE)

    當一個線程獲取後,其他線程嘗試獲取都會失敗

  • 共享模式(SHARED)

    多個線程併發獲取的時候,可能都可以成功

Node中有一個nextWaiter字段,看名字並不像,其實這個是兩個隊列放入共用字段,一個用處是條件隊列下一個節點的指向,另一個可以表示同步隊列節點的模式,可以在下面代碼的SHARED和EXCLUSIVE定義中看到。

因爲只有在獨佔模式下才會有條件隊列,所以只需定義一個共享模式的節點,就可以區分兩個模式了:

/** Marker to indicate a node is waiting in shared mode */
static final Node SHARED = new Node();
/** Marker to indicate a node is waiting in exclusive mode */
static final Node EXCLUSIVE = null;
/**
 * Link to next node waiting on condition, or the special
 * value SHARED.  Because condition queues are accessed only
 * when holding in exclusive mode, we just need a simple
 * linked queue to hold nodes while they are waiting on
 * conditions. They are then transferred to the queue to
 * re-acquire. And because conditions can only be exclusive,
 * we save a field by using special value to indicate shared
 * mode.
 */
Node nextWaiter;

/**
 * Returns true if node is waiting in shared mode.
 */
final boolean isShared() {
    return nextWaiter == SHARED;
}

SHARED是靜態變量,地址不會變更,所以直接使用isShared()方法直接判斷模式。獨佔模式就像普遍認知的鎖能力一樣,比如ReentrantLock。而共享模式支撐了更多作爲同步器的其他需求的能力,比如Semaphore

節點狀態

節點狀態是volatile修飾的int字段waitStatus

  • CANCELLED(1):表示當前節點已取消調度。當timeout或被中斷(響應中斷的情況下),會觸發變更爲此狀態,進入該狀態後的結點將不會再變化。
  • SIGNAL(-1):表示後繼結點在等待當前結點喚醒。後繼結點入隊時,會將前繼結點的狀態更新爲SIGNAL。
  • CONDITION(-2):表示結點等待在Condition上,當其他線程調用了Condition的signal()方法後,CONDITION狀態的結點將從等待隊列轉移到同步隊列中,等待獲取同步鎖。
  • PROPAGATE(-3):共享模式下,前繼結點不僅會喚醒其後繼結點,同時也可能會喚醒後繼的後繼結點。
  • 0:新節點入隊時的默認狀態。

正數表示節點不需要喚醒,所以在一些情況下只需要判斷數值的正負值即可。

AQS獨佔模式源碼

ReentrantLock入手瞭解一下AQS獨佔模式下的源代碼。

測試代碼:

public class AQSTest implements Runnable{

    static ReentrantLock reentrantLock = new ReentrantLock();
    public static void main(String[] args) throws InterruptedException {
        AQSTest aqsTest = new AQSTest();
        Thread t1 = new Thread(aqsTest);
        t1.start();
        Thread t2 = new Thread(aqsTest);
        t2.start();
        t1.join();
        t2.join();
    }
    
    /**
     * 執行消耗5秒
     */
    @Override
    public void run() {
        reentrantLock.lock();
        try {
            TimeUnit.SECONDS.sleep(5);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }finally {
            reentrantLock.unlock();
        }
    }
}

測試代碼模擬了兩個線程爭搶鎖的場景,一個線程先獲取到鎖,另一個線程進入隊列等待,5秒後第一個線程釋放線程,第二個線程獲取到鎖。

獲取鎖

AQS中的acquire方法提供獨佔模式的獲取鎖能力。

/**
 * Acquires in exclusive mode, ignoring interrupts.  Implemented
 * by invoking at least once {@link #tryAcquire},
 * returning on success.  Otherwise the thread is queued, possibly
 * repeatedly blocking and unblocking, invoking {@link
 * #tryAcquire} until success.  This method can be used
 * to implement method {@link Lock#lock}.
 *
 * @param arg the acquire argument.  This value is conveyed to
 *        {@link #tryAcquire} but is otherwise uninterpreted and
 *        can represent anything you like.
 */
@ReservedStackAccess
public final void acquire(int arg) {
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}

先執行tryAcquire成功就結束,失敗就進行入隊等待操作。

入隊

addWaiter

根據傳入的mode爲當前線程創建一個入隊的Node。這裏有一個前提就是執行入隊流程意味着已經發生競爭的情況,這一個前提可以幫助到讀下面的代碼。

/**
 * Creates and enqueues node for current thread and given mode.
 *
 * @param mode Node.EXCLUSIVE for exclusive, Node.SHARED for shared
 * @return the new node
 */
private Node addWaiter(Node mode) {
  	// 創建Node
    Node node = new Node(Thread.currentThread(), mode);
    // Try the fast path of enq; backup to full enq on failure
  	// 先執行一次快速路徑入隊邏輯(在競爭前提下,預判頭尾節點都已經初始化好了)【1】
    Node pred = tail;
  	// 尾節點不爲空
    if (pred != null) {
        node.prev = pred;
      	// 嘗試在尾節點後面入隊 【2】
        if (compareAndSetTail(pred, node)) {
            pred.next = node;
            return node;
        }
    }
  	// 完整的執行路徑放入隊尾
    enq(node);
    return node;
}

/**
 * Inserts node into queue, initializing if necessary. See picture above.
 * @param node the node to insert
 * @return node's predecessor
 */
private Node enq(final Node node) {
  	// 自旋
    for (;;) {
        Node t = tail;
      	// 這個尾節點爲空表示未初始化頭尾節點過 【3】
        if (t == null) { // Must initialize
          	// cas設置頭節點【4】
            if (compareAndSetHead(new Node()))
              	// 尾節點和頭節點保持一致
                tail = head;
        } else {// 這個分支和前面的快速路徑入隊邏輯一致【5】
            node.prev = t;
            if (compareAndSetTail(t, node)) {
                t.next = node;
                return t;
            }
        }
    }
}

/**
 * Head of the wait queue, lazily initialized.  Except for
 * initialization, it is modified only via method setHead.  Note:
 * If head exists, its waitStatus is guaranteed not to be
 * CANCELLED.
 */
private transient volatile Node head;

/**
* Tail of the wait queue, lazily initialized.  Modified only via
 * method enq to add new wait node.
 */
private transient volatile Node tail;

/**
 * CAS head field. Used only by enq.
 */
private final boolean compareAndSetHead(Node update) {
    return unsafe.compareAndSwapObject(this, headOffset, null, update);
}

/**
 * CAS tail field. Used only by enq.
 */
private final boolean compareAndSetTail(Node expect, Node update) {
    return unsafe.compareAndSwapObject(this, tailOffset, expect, update);
}
  • 【1】,快速路徑入隊,這個fast path的思路在本類其他代碼中也有,我的理解是在代碼分支上預判某個分支是大多數情況發生的分支,所以優先執行,如果不是沒有進入再走完整兜底代碼。這裏enq就是完整兜底代碼,其中有處理頭尾節點初始化邏輯,因爲頭尾節點是隊列生命週期中只執行一次的操作,大部分場景是不需要考慮初始化頭尾節點的分支,所以纔有了這裏所謂的fast path

  • 【2】,cas操作尾節點成功,才執行尾部入隊的最後一步操作:原尾節點的next指向自己。對於一個雙向鏈表,在尾部插入一個元素需要兩步:A,自己的prev指向當前的尾節點;B,當前尾節點的next指向自己。而在AQS的同步隊列裏還有一個tail指向當前尾節點,所以又多了一步就是需要把tail指向自己,一共三步。回過頭再仔細閱讀下代碼,它的操作步驟是,先設置自己prev指向可能的尾節點,然後cas操作tail(compareAndSetTail)指向到自己,如果成功,就更新尾節點的next指向自己。在併發場景中,cas是可能失敗的,所以自己的prev可能需要不斷地變更,而當前隊列中的尾節點的next是在cas設置tail後才操作,只變更一次。

  • 【3】,頭尾節點都是延遲初始化(lazily initialized),在沒有需要入隊操作前都不會進行初始化。初始化就是new出一個waitstatus爲0的Node設置給head,然後尾節點賦值(tail = head;)。

  • 【4】【5】,初始化頭尾節點由兩步操作組成,頭節點cas設置成功後,纔會設置尾節點,所以可以確定只要尾節點不爲null,頭節點就一定不爲空。

    假設compareAndSetHead成功設置head後,執行尾節點賦值時尾節點會不會已經被其他線程修改了呢?

    不會,因爲compareAndSetHead操作只在enq方法調用,也只有在頭節點未初始化時觸發,而如果初始化頭節點成功後,此時的tail還一定是null,所以前面的邏輯裏都進不了操修改tail不爲null的分支代碼,只能進入初始化頭尾節點的分支,所以會在compareAndSetHead上自旋,直到tail設置結束,就可以進入tail不爲null的分支代碼了。再仔細想一下這個設計只要先判斷的是tail是否爲空就相當於判斷了初始化是否結束。

下圖是這種場景同步隊列節點變化情況:
image

  • 1,初始時同步隊列的head和tail都爲null,state是0
  • 2,當第一個線程獲得鎖,就會把state置成1,此時head和tail都爲null,因爲還沒出現競爭情況,沒有必要初始化頭尾節點。而當再有線程來獲取鎖的時候就需要進行入隊等待了,enq方法中自旋的第一次循環會觸發初始化頭尾節點,這個節點的thread是null,waitStatus是初始化狀態0,next和prev的指向也都是null。
  • 3,初始化好頭尾節點後,接下去就是把新創建的Node放到同步隊列的尾部。

acquireQueued

前面已經在隊列裏入隊成功,然而線程還沒進入等待狀態,接下去自然是把線程轉成等待了,就像物理上已經處理好入隊了,還差法術上的入隊等待了。

/**
 * Acquires in exclusive uninterruptible mode for thread already in
 * queue. Used by condition wait methods as well as acquire.
 *
 * @param node the node
 * @param arg the acquire argument
 * @return {@code true} if interrupted while waiting
 */
@ReservedStackAccess
final boolean acquireQueued(final Node node, int arg) {
  	// 標識是否獲取失敗
    boolean failed = true;
    try {
      	// 標識線程是否中斷(等待是在下面的自旋中,將來喚醒後會檢查線程中斷狀態)
        boolean interrupted = false;
      	// 自旋【1】
        for (;;) {
          	// 獲得當前節點的前節點
            final Node p = node.predecessor();
          	// 如果前面已經是頭節點了,那麼代表機會來了,進行一次tryAcquire,嘗試獲取鎖【2】
            if (p == head && tryAcquire(arg)) {
              	// 更新頭節點
                setHead(node);
              	// 斷開前節點next引用
                p.next = null; // help GC
                failed = false;
                return interrupted;
            }
          	// 檢查是否需要park 需要的話就進行線程等待【3】
          	// shouldParkAfterFailedAcquire這個方法邏輯就是我要躺平休息了得確定前面有人能叫醒我
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        if (failed)
          	// 取消嘗試獲取鎖的節點
            cancelAcquire(node);
    }
}
/**
 * Checks and updates status for a node that failed to acquire.
 * Returns true if thread should block. This is the main signal
 * control in all acquire loops.  Requires that pred == node.prev.
 *
 * @param pred node's predecessor holding status
 * @param node the node
 * @return {@code true} if thread should block
 */
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
  	// 前節點的waitStatus
    int ws = pred.waitStatus;
  	// 已經是SIGNAL狀態,就直接返回
    if (ws == Node.SIGNAL)
        /*
         * This node has already set status asking a release
         * to signal it, so it can safely park.
         */
        return true;
  	// 如果是大於0的狀態表示前面節點是取消狀態,但是還沒有從隊列上移除
    if (ws > 0) {
        /*
         * Predecessor was cancelled. Skip over predecessors and
         * indicate retry.
         */
        do {
          	// 這裏移除狀態大於0的節點,就是把當前節點的prev往前移
            node.prev = pred = pred.prev;
          // 前移直到找到一個不是取消狀態的節點
        } while (pred.waitStatus > 0);
      	// 前節點next設置(雙向鏈表常規操作)
        pred.next = node;
    } else {
        /*
         * waitStatus must be 0 or PROPAGATE.  Indicate that we
         * need a signal, but don't park yet.  Caller will need to
         * retry to make sure it cannot acquire before parking.
         */
      	// cas設置前節點狀態爲SIGNAL,這個cas操作需要外面的調用方再一次確認是否真的不能獲取鎖後再進行park操作
        compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
    }
    return false;
}
/**
 * CAS waitStatus field of a node.
 */
private static final boolean compareAndSetWaitStatus(Node node,
                                                     int expect,
                                                     int update) {
    return unsafe.compareAndSwapInt(node, waitStatusOffset,
                                    expect, update);
}
/**
 * Sets head of queue to be node, thus dequeuing. Called only by
 * acquire methods.  Also nulls out unused fields for sake of GC
 * and to suppress unnecessary signals and traversals.
 *
 * @param node the node
 */
// 將傳入的節點設置爲head,並且抹去節點中不必要的引用,注意沒有cas操作,
private void setHead(Node node) {
    head = node;
    node.thread = null;
    node.prev = null;
}
/**
 * Convenience method to park and then check if interrupted
 *
 * @return {@code true} if interrupted
 */
private final boolean parkAndCheckInterrupt() {
    LockSupport.park(this);
    return Thread.interrupted();

  • 【1】這個自旋里,有修改前節點狀態失敗或者前節點有取消的狀態情況而需要自旋。

  • 【2】如果前節點已經是head,那麼意味着自己有資格爭奪鎖資源,當然如果沒有獲取到,那還是乖乖走等待的邏輯,如果獲取到,表示此前面節點入隊的時候沒有獲取到鎖,而此時鎖已經釋放,那麼自己就會成爲獲得鎖的線程,隊列中自己節點就會替換當前頭節點成爲新的head。

    方法setHead沒有做自旋操作,是簡單幾個賦值操作集合,因爲這個方法是確保tryAcquiretryAcquireShared成功後執行的,所以不需要考慮併發情況。方法中會把thread和prev都置空,其實獲取到鎖的節點內這兩個信息已經沒什麼作用,並且自己的前節點的next也會值空,切斷對自己引用。

  • 【3】同步隊列中是看自己Node裏的waitStatus是什麼來決定是否喚醒後節點,如果是SIGNAL狀態,就會喚醒後節點。所以每個排隊的節點在自己進入等待狀態前都需要確保前節點的狀態是SIGNAL狀態,這樣就可以保證未來是可以被喚醒的。這就是shouldParkAfterFailedAcquire方法做的事。

    shouldParkAfterFailedAcquire方法名也明確表達了這個是線程park的前置條件判斷,只要這個方法返回true,線程就可以安心去等待了。具體方法實現代碼我們詳細再繼續往下看會發現,只有判斷出前節點waitStatus是SIGNAL狀態纔會返回true,其他還有兩種情況:A,前節點狀態爲取消狀態,就會進行前節點引用前移,直到前節點不是取消節點,然後退出方法繼續自旋;B,前節點是0或者PROPAGATE狀態,就進行cas修改爲SIGNAL狀態,無論成功或失敗都是退出繼續自旋。所以前節點除了已經是SIGNAL狀態,其他情況都會再進行自旋,自旋的開始就會進行一次頭節點的判斷,以保證本次自旋在head後節點能夠快速進行一次獲取操作。上面【2】中提過,在沒有獲取到的情況下還是會走等待的邏輯,那麼也就是說head節點的waitstatus狀態必須已經是SIGNAL狀態了。

延續前面的測試代碼,繼續圖解節點數據的變化:
image-20220201233215428

補充說明:

因爲測試代碼是一個線程獲取鎖,一個線程等待,所以隊列中只會有兩個節點一個head,一個等待節點,在等待節點設置前節點waitStatus的自旋代碼中對前節點是否爲head的判斷就爲true,所以在第一次自旋的時候會執行一次tryAcquire,然後執行shouldParkAfterFailedAcquire後將head節點的waitStatus更新爲SIGNAL狀態後再會自旋執行一次tryAcquire,因爲前節點還未釋放鎖,所以兩次tryAcquire都失敗,然後才執行park,線程進入等待狀態。

acquireQueued方法中的最後finally代碼塊中,判斷failed字段是否爲true,如果是就會執行cancelAcquire方法取消節點,那麼什麼時候會發生failed爲true的情況呢?已經有同學也思考過這個問題。我還有一個理解是:本來AQS就是以一個框架形式提供,子類實現一些方法達成自己想要的同步器形式,這裏的tryAcquire方法就是子類實現的,既然是子類擴展實現的那就沒法保證這個方法是否會跑出遺產中斷自旋而導致執行到cancelAcquire方法。

順便也讀下cancelAcquire方法的源碼:

/**
 * Cancels an ongoing attempt to acquire.
 *
 * @param node the node
 */
private void cancelAcquire(Node node) {
    // Ignore if node doesn't exist
    if (node == null)
        return;

    node.thread = null;

    // Skip cancelled predecessors
  	// 這段遇到取消狀態節點就把節點前移代碼和shouldParkAfterFailedAcquire一致
    Node pred = node.prev;
    while (pred.waitStatus > 0)
        node.prev = pred = pred.prev;

    // predNext is the apparent node to unsplice. CASes below will
    // fail if not, in which case, we lost race vs another cancel
    // or signal, so no further action is necessary.
    Node predNext = pred.next;

    // Can use unconditional write instead of CAS here.
    // After this atomic step, other Nodes can skip past us.
    // Before, we are free of interference from other threads.
    node.waitStatus = Node.CANCELLED;

    // If we are the tail, remove ourselves.
  	// 如果自己是尾節點,操作就比較簡單,cas操作tail指向,然後把前節點的prev指向設置成null就結束了
    if (node == tail && compareAndSetTail(node, pred)) {
        compareAndSetNext(pred, predNext, null);
    } else {
        // If successor needs signal, try to set pred's next-link
        // so it will get one. Otherwise wake it up to propagate.
        int ws;
      	// 不是tail,也不是head的後節點,判斷waitStatus是不是SIGNAL,如果不是就cas設置一次爲SIGNAL
        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 {
          	// 前節點是head,此時自己的waitStatus是CANCELLED,unparkSuccessor會跳過自己節點去喚醒自己後符合條件的節點
            unparkSuccessor(node);
        }

        node.next = node; // help GC
    }
}

釋放鎖

/**
 * Releases in exclusive mode.  Implemented by unblocking one or
 * more threads if {@link #tryRelease} returns true.
 * This method can be used to implement method {@link Lock#unlock}.
 *
 * @param arg the release argument.  This value is conveyed to
 *        {@link #tryRelease} but is otherwise uninterpreted and
 *        can represent anything you like.
 * @return the value returned from {@link #tryRelease}
 */
@ReservedStackAccess
public final boolean release(int arg) {
  	// 首先就進行一次釋放操作【1】
    if (tryRelease(arg)) {
      	// 持有鎖的節點永遠是頭節點【2】
        Node h = head;
        if (h != null && h.waitStatus != 0)
          	// 喚醒後節點線程
            unparkSuccessor(h);
        return true;
    }
    return false;
}
  • 【1】這個釋放操作是先執行的,只有成功纔會進入從頭節點往後喚醒後節點的操作,所以在後續unparkSuccessor的代碼邏輯中是有這個重要前提條件的,需要特別注意。
  • 【2】這裏head是不可能爲null的,這個是由整個同步隊列機制決定的,無論是初始化的頭節點還是後面將看到的被喚醒獲得鎖的節點替換成爲頭節點,可以認爲頭節點表示着獲取鎖的節點,雖然這個頭節點是不維護線程。然後會判斷head的waitStatus狀態不爲0,因爲前面入隊代碼中已經提過在把自己線程park前會需要先把前節點設置成SIGNAL狀態。

假設測試代碼中的unlock執行,節點數據的變化如下圖:
image-20220203115247292

喚醒後節點線程

unparkSuccessor

/**
 * Wakes up node's successor, if one exists.
 *
 * @param node the 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.
     */
    int ws = node.waitStatus;
  	// 只要狀態是小於0,就進行一次cas設置爲0【1】
    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;
  	// 此時沒有後節點或者後節點狀態是取消狀態【2】
    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)
      	// 喚醒節點持有線程【3】
        LockSupport.unpark(s.thread);
}
  • 【1】執行一次cas重置waitStatus,不過沒有自旋加持,所以是允許失敗的
  • 【2】在註釋中信息是這樣:需要喚醒的線程在下一個節點上,如果從next指向的節點不符合喚醒的節點(null或狀態爲取消),那麼就從隊列尾部開始往前找那個沒有取消的節點,當然也有可能沒找到需要喚醒的節點。註釋沒有說明爲什麼需要這麼做,我們再回顧下放入隊列尾部節點的代碼分析(入隊【2】),compareAndSetTail成功保證了當前節點的prev和隊列的tail的指向是成功的,而最後一步pred.next指向是在cas操作成功後執行的,會有這樣的場景就是cas執行成功還沒執行到pred.next指向操作,那麼此時隊列從前往後找一個沒有取消的節點會找到的是null,而從尾往前遍歷就沒有問題。
  • 【3】unpark操作對應的前面入隊等待park操作,也就是說喚醒的線程會從那時等待的地方繼續往下執行。繼續執行的代碼就是acquireQueued中自旋的部分。所以當喚醒等待的線程後自旋代碼就會檢查自己節點的前面是不是head,如果是就會進行一次獲取鎖操作,如果不是就執行shouldParkAfterFailedAcquire方法。

按前面例子裏的unlock觸發釋放鎖,先執行unparkSuccessor方法更新頭節點的waitStatus爲0,然後會unpark後節點線程,被喚醒的線程開始執行acquireQueued方法的自旋,判斷當前線程節點的前節點就是head,那麼就會執行tryAcquire返回成功,然後開始替換頭節點。

隊列節點數據變化如圖:

image-20220203163342984

ReentrantLock源碼

ReentrantLock基於AQS實現的可重入鎖,支持公平和非公平。

進行了前面AQS代碼的解析,ReentrantLock的代碼變得異常簡單,考慮到篇幅有限,下面只對公平性和可重入性進行解析,在後續文章中的還會再使用ReentrantLock。

公平/非公平

ReentrantLock內部實現了一個內部抽象類Sync,它的子類有FairSyncNonfairSync,看名字就明白了具體公平和非公平就是這兩個類的實現不同了。

AQS內置的FIFO同步隊列,入隊後天然是公平的,什麼時候會出現不公平的情況呢?

在這裏的不公平是指:一個剛來獲取資源的線程會和已經在隊列中排隊的線程產生競爭,隊列裏等待的線程運氣不好一點始終競爭不過新來的線程,而新來的線程假如源源不斷過來,隊列裏等待的線程獲取成功等待的時間就很長,那麼就會出現所謂的線程飢餓問題,這個就是這裏需要解決的不公平。而公平就是按入隊的順序來決定獲取資源的順序,那麼這個新來的線程就應該在所有已經入隊的線程之後再來獲取。要達到這個公平的效果就是每個線程進來獲取的時候,先判斷一下是否有其他線程已經在等待獲取資源了,如果有就不用去獲取了,直接去入隊就行了。

hasQueuedPredecessors

判斷的方法就是hasQueuedPredecessors

/**
* Queries whether any threads have been waiting to acquire longer
* than the current thread.
**/
public final boolean hasQueuedPredecessors() {
    // The correctness of this depends on head being initialized
    // before tail and on head.next being accurate if the current
    // thread is first in queue.
    Node t = tail; // Read fields in reverse initialization order
    Node h = head;
    Node s;
    return h != t &&
        ((s = h.next) == null || s.thread != Thread.currentThread());
}

截取部分方法註釋:只要有線程等待的時間比當前線程長就應該返回true,否則返回false。

雖然這個方法的判斷代碼不多,可是直接看會有點懵,但是有了前面的代碼解析鋪墊,這個代碼瞬間看懂。

一個關鍵的關聯信息是前面介紹的enq方法中初始化頭尾節點,我們已經知道初始化頭尾節點不是原子操作,分成兩步操作:

   compareAndSetHead(new Node()) // 1
   tail = head // 2

所以就從初始化頭尾節點的角度來分析下這個判斷,以下是三種場景下的情況的解析:

  • 1,完全未初始化,也就是沒有出現過競爭場景,所有head和tail都是null,h != t 爲false,返回false
  • 2,初始化到一半,也就是執行完compareAndSetHead(new Node())還沒執行tail = head;,tail爲null,head不等於null,h != t 爲true,因爲此時head的next指向還是爲空的,(s = h.next) == null爲true,返回true。這個場景意味着有線程因競爭而觸發初始化頭尾節點,雖然還沒有進行入隊成功,但還是認爲它是先於當前線程的。
  • 3,初始化結束,對於h != t 有幾種情況:
    • 隊列中只有head節點,也沒有獲取成功的線程,因爲已經初始化結束,所以head和tail指向同一個對象,h != t 爲false,返回false
    • 隊列中只有head節點,有獲取成功的線程,因爲已經初始化結束,所以head和tail指向同一個對象,h != t 爲false,返回false,看起來和第一種情況相同,展開還有以下情況
      • 沒有線程在入隊,這種情況就是隻有當前這個線程在獲取操作,所以不需要排隊,返回false沒問題
      • 有線程正在入隊,只是compareAndSetTail操作還未成功,這種場景也可以是不考慮的,因爲對於入隊的先後順序是cas操作,代碼在cas未成功前並不確定哪個線程的先後情況。
    • 隊列中除head節點還有1個節點情況,這個情況就是head後的節點入隊成功,表示保證了compareAndSetTail操作成功,h != t 爲true,那麼也有兩種場景,
      • 已經執行過head的next指向操作( t.next = node),(s = h.next) == null是false,返回的結果就是 s.thread != Thread.currentThread()的結果(如果第二個節點是自己返回true,如果不是返回false)
      • 還沒有執行head的next指向操作,(s = h.next) == null是true,返回true,這個場景和頭尾初始化到一半一樣也是入隊操作到一半的情況。

因爲節點有中斷,取消,超時的情況,所以這個方法無法保證返回的結果在節點狀態併發變化情況下的正確性。

有必要理解一下Read fields in reverse initialization order這個註釋。網上也有人問,爲什麼獲取tail要先於獲取head呢?

本質原因還是因爲初始化頭尾節點也是有順序性的,必然是cas設置head成功後,tail纔會被設置。這裏的讀取順序因爲tail和head都是volatile修飾也是不會被重排序的。這裏不詳細描述各種併發情況,只假設先讀head再讀tail下會有問題的場景

如果是先讀head再讀tail,有以下這個場景會有問題,這個應該一看就明白了:

image-20220209171433212

那麼head爲null,tail不爲null,h != t 爲true,然後執行(s = h.next) == null就會空指針。那麼先獲取tail再獲取head難道就沒有問題了嗎,有興趣的同可以自己推演下倒序讀取的各種場景。

實現

ReentrantLock中的FairSync類負責提供公平鎖的能力,核心就是自定義的tryAcquire方法

/**
 * Fair version of tryAcquire.  Don't grant access unless
 * recursive call or no waiters or is first.
 */
@ReservedStackAccess
protected final boolean tryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();
  	// 鎖未被獲取狀態
    if (c == 0) {
      	// 先使用hasQueuedPredecessors判斷是否需要排隊,返回false才進行一次cas競爭
        if (!hasQueuedPredecessors() &&
            compareAndSetState(0, acquires)) {
          	// 設置當前獲取到鎖的線程
            setExclusiveOwnerThread(current);
            return true;
        }
    }
  	// 鎖被獲取狀態 判斷是不是自己獲取的鎖
    else if (current == getExclusiveOwnerThread()) {
        int nextc = c + acquires;
        if (nextc < 0)
            throw new Error("Maximum lock count exceeded");
        setState(nextc);
        return true;
    }
    return false;
}

而公平鎖的tryAcquire方法實現的區別就是沒有!hasQueuedPredecessors的判斷,其他代碼一模一樣。有了hasQueuedPredecessors方法的理解,這個公平鎖實現就更加深刻了。

可重入

可重入就是支持一個線程多次獲取鎖的能力,在釋放鎖的時候也需要多次釋放。這個實現在Sync#nonfairTryAcquire方法和FairSync#tryAcquire方法中有體現,就是用if (current == getExclusiveOwnerThread())判斷如果是當前線程,就累加state。

tryRelease的實現也對可重入的邏輯進行了處理:

protected final boolean tryRelease(int releases) {
    int c = getState() - releases;
    if (Thread.currentThread() != getExclusiveOwnerThread())
        throw new IllegalMonitorStateException();
    boolean free = false;
    if (c == 0) {
        free = true;
      	// 只有state被減到0的時候纔會設置
        setExclusiveOwnerThread(null);
    }
    setState(c);
    return free;
}

總結

本文直接對AQS源碼的核心結構和源代碼進行了詳細的分析,然後使用ReentrantLock作爲實現的同步器進行了部分了解,爲後續JUC中類的源碼解讀打下基礎。本文涉及的內容是jdk中各種同步器實現基礎的核心部分,個人精力有限,不正之處望留言指出。

本文已在公衆號上發佈,感謝關注,期待和你交流。
image-20220209171433212

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