天天看點

AQS詳解之獨占鎖模式AQS 介紹AQS原理AQS資料結構AQS添加節點AQS重要方法AQS獨占鎖模式AQS獨占鎖擷取流程圖獨占鎖釋放

AQS 介紹

AbstractQueuedSynchronizer簡稱AQS,即隊列同步器。它是JUC包下面的核心元件,它的主要使用方式是繼承,子類通過繼承AQS,并實作它的抽象方法來管理同步狀态,它分為獨占鎖和共享鎖。很多同步元件都是基于它來實作的,比如我門常見的ReentrantLock,它是基于AQS的獨占鎖實作的,它表示每次隻能有一個線程持有鎖。在比如ReentrantReadWriteLock它是基于AQS的共享鎖實作的,它允許多個線程同時擷取鎖,并發的通路資源。AQS是建立在CAS上的一種FIFO的雙向隊列,它通過維護一個int類型的state,這個state是用volatile來修飾,進而保證狀态的安全行。

AQS對于狀态的更改提供了3個方法:

  1. getState() :傳回同步狀态的目前值
  2. setState() : 設定目前同步狀态
  3. compareAndSetState():使用CAS設定目前狀态,該方法能夠保證狀态的原子性。它是通過Unsafe這個類中的native方法來保證的。

AQS原理

如果請求的共享資源空閑,那麼就把目前請求的線程設定為工作線程,并且将共享資源設定為鎖定狀态。如果被請求的共享資源占用,那麼需要一套線程阻塞等待以及喚醒的鎖的配置設定機制。那麼這套機制AQS是用CLH隊列鎖實作的,擷取不到鎖的線程将加入到隊列中。AQS内部維護的一個同步隊列,擷取失敗的線程會加入到隊列中進行自旋,移除隊列條件是前驅節點是頭節點并且成功擷取到了同步狀态,釋放同步狀态AQS會調用unparkSuccessor方法喚醒後繼節點。

AQS資料結構

AQS隊列内部維護的是一個FIFO的雙向連結清單,如下圖。這種結構的特點是每個資料結構都有2個指針,分别指向直接前驅節點和直接的後繼節點。這種結構可以從任意的一個節點開始很友善的通路前驅和後繼節點。每個Node由線程封裝,當競争失敗後會加入到AQS隊列中去。

AQS詳解之獨占鎖模式AQS 介紹AQS原理AQS資料結構AQS添加節點AQS重要方法AQS獨占鎖模式AQS獨占鎖擷取流程圖獨占鎖釋放

下面具體看一下Node組成:

static final class Node {
    /** 表示節點正處于共享模式下等待标記 */
    static final Node SHARED = new Node();
    /** 表示節點處于獨占鎖模式的等待标記 */
    static final Node EXCLUSIVE = null;
    /** waitStatus值,表示線程取消 */
    static final int CANCELLED =  1;
    /** waitStatus值,表示線程需要挂起 */
    static final int SIGNAL    = -1;
    /** waitStatus值,表示線程處于等待條件*/
    static final int CONDITION = -2;
    /**waitStatus值,表示下一個共享模式應該無條件傳播*/
    static final int PROPAGATE = -3;
    /**狀态字段*/
    volatile int waitStatus;
    /**前驅節點 */
    volatile Node prev;
    /**後繼節點 */
    volatile Node next;
    /**目前線程*/
    volatile Thread thread;
    /**将此節點入列的線程,用來來接下一個節點*/
    Node nextWaiter;
    /**如果節點在共享模式下等待,則傳回true*/
    final boolean isShared() {
        return nextWaiter == SHARED;
    }
    /**傳回上一個節點,如果為null則抛出異常,前驅節點不是null使用 */
    final Node predecessor() throws NullPointerException {
        Node p = prev;
        if (p == null)
            throw new NullPointerException();
        else
            return p;
    }
    Node() {    // 用于建立初始化head節點
    }
    Node(Thread thread, Node mode) {     // 由addWaiter使用
        this.nextWaiter = mode;
        this.thread = thread;
    }
    Node(Thread thread, int waitStatus) { // 由Condition使用
        this.waitStatus = waitStatus;
        this.thread = thread;
    }
}           

AQS添加節點

AQS将節點加入到同步隊列的過程圖,如下:

AQS詳解之獨占鎖模式AQS 介紹AQS原理AQS資料結構AQS添加節點AQS重要方法AQS獨占鎖模式AQS獨占鎖擷取流程圖獨占鎖釋放

加入隊列的過程必須是線程安全的,是以AQS提供了一個基于CAS設定尾節點的方法compareAndSetTail,這個也是unsafe類中的native方法。它需要傳入目前線程的認為的尾節點和目前節點,當設定成功後,目前節點和尾部節點建立關聯,目前節點正式加入到隊列。

AQS重要方法

AQS使用了模版方法模式,自定義同步器需要重寫下面的幾個AQS提供的模版方法:

isHeldExclusively()//該線程是否處于獨占資源。隻有用到condition才需要實作它.
tryAcquire(int)//獨占方式擷取資源,成功傳回true,失敗傳回false
tryRelease(int)//獨占方式釋放資源,成功傳回true,失敗傳回false
tryAcquireShared(int)//共享方式擷取資源。負數表示失敗,0表示成功但是沒有剩餘可用資源;正數表示成功且有剩餘資源
tryReleaseShared(int)//共享方式釋放資源.成功傳回true,失敗傳回false.           

AQS獨占鎖模式

獨占鎖的擷取是通過AQS提供的acquire()。我門看一下這個方法的源代碼:

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

發現acquire()擷取同步狀态成功與否做了2件事情。1成功,方法結束傳回,2失敗,會将目前線程加入到同步隊列,它是通過調用addWaiter()和acquireQueued()方法實作的,我門繼續看一下這2個方法的源代碼.

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;
      }
  }
   enq(node);
   return node;
}           

通過方法會發現它會先把目前線程封裝為Node類型,然後判斷尾節點是否為空,如果不為空進行CAS操作入隊列,如果為空,那麼會調用enq()這個方法,此方法做了通過不斷的for循環自旋CAS尾插入節點。

現在我門已經明白獨占鎖擷取失敗入隊列的過程了,那麼對于同步隊列的節點會做什麼事情來保證自己有機會擷取獨占鎖呢?我門來看一下acquireQueued()這個方法的源代碼

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) &&
               parkAndCheckInterrupt())//擷取失敗調用的方法
               interrupted = true;
      }
  } finally {
       if (failed)
           cancelAcquire(node);
  }
}           

從源代碼我門可以看出來這是一個自旋過程(for(;;)),它首先擷取目前節點的前驅節點,然後判斷目前節點能否擷取獨占鎖,如果前驅節點是頭節點并且擷取同步狀态,那麼就可以擷取到獨占鎖。如果擷取鎖失敗線程會進入等待狀态等待擷取獨占鎖。

shouldParkAfterFailedAcquire()這個方法主要的邏輯是調用compareAndSetWaitStatus(),使用CAS将節點狀态由INITIAL設定為SIGNAL。如果失敗會傳回false,通過acquireQueued()的自旋轉會繼續設定,直到設定成功。設定成功後調用parkAndCheckInterrupt()方法,此方法會調用LockSupport.park(this)讓該線程阻塞。到此獨占鎖擷取過程已經分析完畢了。

AQS獨占鎖擷取流程圖

AQS詳解之獨占鎖模式AQS 介紹AQS原理AQS資料結構AQS添加節點AQS重要方法AQS獨占鎖模式AQS獨占鎖擷取流程圖獨占鎖釋放

獨占鎖釋放

獨占鎖的釋放是用relase()方法,我門來看一下源代碼

public final boolean release(int arg) {
   if (tryRelease(arg)) {
       Node h = head;
       if (h != null && h.waitStatus != 0)
           unparkSuccessor(h);
       return true;
  }
   return false;
}           

這段代碼的邏輯就很容易了解了,如果同步狀态釋放成功,則執行if語句内的代碼,當head不為空并且狀态不為0的時候會執行unparkSuccessor()方法,unparkSuccessor方法會執行LookSupport.unpark()方法.每一次釋放鎖就會喚醒隊列中該節點的後繼節點,可以進一步的說明擷取鎖是一個先進先出的過程。