JAVA AQS 實現原理

JavaDog發表於2019-03-23

AQS(AbstractQueuedSynchronizer)是java lock的實現基礎,如下類圖描述,juc的很多實現都依賴AQS。AQS確實比較複雜,
所有不同鎖的每個細節難以描述清楚,所以本文的重點是使用ReentrantLock作為例子只分析NonfairSync和condition的實現原理,
其它鎖的實現原理都是以此作為基礎進行擴充套件和優化。

AQS_

從Lock的一段簡單程式碼開始

就以下面一段我們經常使用的簡單程式碼作為例子,例子中就是使用Lock和Condition實現執行緒安全的生產者-消費者模型,藉助這個簡單的例子,
我們一步步分析一下Lock內部是如何實現執行緒同步的。

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

    public class ProductQueue<T> {

    private T[]       items;
    private Lock      lock         = new ReentrantLock();
    final static int  DEAFULT_SIZE = 10;

    //滿佇列條件
    private Condition notFull      = lock.newCondition();

    //空佇列條件
    private Condition notEmpty     = lock.newCondition();

    private int       head, tail, count;

    @SuppressWarnings("unchecked")
    public ProductQueue(int size) {
        items = (T[]) new Object[size];
    }

    public ProductQueue() {
        this(DEAFULT_SIZE);
    }

    /**
     * 生產產品
     * 
     * @param t
     * @throws InterruptedException
     */
    public void put(T t) throws InterruptedException {
        lock.lock();//獲取獨佔鎖
        try {
            Thread.sleep(2 * 1000);
            while (count == getCapacity()) {//佇列已滿,掛起當前 執行緒,將當前執行緒加入條件鎖佇列中,直到收到佇列不為滿的訊號,從佇列中按照FIFO喚醒一個執行緒
                notFull.await(); //釋放獨佔鎖 ,讓其他執行緒(消費者執行緒)獲取,然後掛起當前執行緒,一旦後續條件滿足,再次獲取鎖
            }
            items[tail] = t;
            if (++tail == getCapacity()) {
                tail = 0;
            }
            ++count;
            notEmpty.signalAll();//佇列已不為空,可以喚醒消費者 消費產品
        } finally {
            lock.unlock();
        }

    }

    /**
     * 消費產品
     * 
     * @return
     * @throws InterruptedException
     */
    public T get() throws InterruptedException {
        lock.lock();
        try {
            while (count == 0) {//佇列已空,掛起空佇列鎖條件 執行緒,直到收到不為空的訊號,被喚醒
                notEmpty.await();
            }
            T ret = items[head];
            items[head] = null;
            if (++head == getCapacity()) {
                head = 0;
            }
            --count;
            notFull.signalAll();//佇列已不為滿,可以喚醒生產者  生產產品
            return ret;

        } finally {
            lock.unlock();
        }
    }

    /**
     * 產品佇列容量
     * 
     * @return
     */
    public int getCapacity() {
        return items.length;
    }

    /**
     * 當前產品數量
     * 
     * @return
     */
    public int size() {
        lock.lock();
        try {
            return count;
        } finally {
            lock.unlock();
        }
    }

}

複製程式碼

AQS基礎資料結構

在開始分析之前,先講一下AQS實現中的基礎資料結構(如下圖所示)
AQS_

  • state:32位整數描述的狀態位來支援原子操作(volatile+CAS)
  • head:有序佇列的隊頭結點,tail:有序佇列的隊尾結點
  • pre:當前節點的前一個結點引用 next:當前節點的下一個節點引用主要用於構建有序佇列的雙向連結串列
  • thread:當前節點繫結的執行緒
  • waitStatus:當前節點的等待狀態
  • nextWaiter:當前節點的後繼節點,主要結合Condition使用
  • firstWaiter:Condition佇列的第一個Node
  • lastWaiter:Condition佇列的最後一個Node,主要用於表示Condition佇列的頭和尾節點,需要區分AQS佇列的head和tail

Lock.lock()

從Lock的lock方法作為入口講起,參考下面的處理流程圖:

lock_

整個加鎖執行邏輯:

  • 1.如果tryAcquire(arg)成功,返回true,說明當前執行緒已經拿到鎖,執行當前執行緒操作,整個lock()過程就結束了。如果失敗進行操作2。
  • 2.建立一個獨佔節點(同時會新生成一個傀儡頭結點)並且將此節點加入CHL佇列末尾。進行操作3。
  • 3.acquireQueued(node,arg),自旋嘗試獲取鎖,如果獲取失敗,則根據前一個節點來決定是否掛起(park()),不管是否掛起,都會自旋,直到成功獲取到鎖。如果在自旋的過程中執行緒被中斷過,那麼會置執行緒中斷標誌, 進行操作4。
  • 4.如果當前執行緒已經中斷過,那麼就中斷當前執行緒(清除中斷位)。

Lock.unlock()

unlock_

整個解鎖執行邏輯:

  • 1.tryRelease(arg)執行解鎖操作,如果鎖釋放失敗或者當前執行緒沒有持有鎖,則丟擲異常(IllegalMonitorStateException),如果成功,則進行操作2。
  • 2.獲取CHL佇列頭結點,如果頭結點為空或者狀態為0,說明CHL佇列為空,結束鎖釋放過程,否則進行操作3。
  • 3.找到需要喚醒的繼任節點,並進行執行緒喚醒操作(unpark()),同時會維護CHL佇列,去除已中斷或者超時的節點,結束鎖釋放過程。

AQS佇列

AQS_

AQS佇列
上面lock()和unlock()2個方法的執行過程中,就維護了一個AQS佇列:

  • 整個AQS佇列為帶頭結點和尾節點的雙向連結串列,其中的節點主要以waitStatus來表示其狀態,並繫結對應的執行緒。
  • AQS佇列的頭節點為傀儡節點,喚醒的節點為傀儡節點的繼任節點,移除節點從佇列頭節點開始,新增節點從佇列尾節點開始,新增時主要以CAS操作保證執行緒安全。
  • AQS佇列主要按FIFO來保證節點的執行順序,以此來達到公平性。

Condition.await()

condit_await

執行邏輯如下:

  • 1.將當前執行緒加入Condition鎖佇列。特別說明的是,這裡不同於AQS的佇列,這裡進入的是Condition的FIFO佇列。
  • 2.釋放鎖,這裡可以看到將鎖釋放了,否則別的執行緒就無法拿到鎖而發生死鎖。進行3。
  • 3.自旋(while)掛起,直到被喚醒或者超時或者CACELLED。進行4。
  • 4.獲取鎖(acquireQueued),並將自己從Condition的FIFO佇列中釋放,表明自己不再需要鎖(我已經拿到鎖了)。

Condition.signal()

condition_signal

signal就是喚醒Condition佇列中的第一個非CANCELLED節點執行緒,
而signalAll就是喚醒所有非CANCELLED節點執行緒。當然了遇到CANCELLED執行緒就需要將其從FIFO佇列中剔除。

aqs和condition2個佇列

aqs_condition_
AQS和Condition佇列

  • 以生產者-消費者模型來說,在產品的生成和消費過程中,會維護3個佇列,一個AQS佇列,一個生產者條件佇列,一個消費者條件佇列。
  • AQS和Condition佇列有一定的不同,條件佇列為典型的FIFO佇列,而AQS帶有一定的特性(頭結點和繼任節點關係)。
  • 當執行await()會向相應的條件佇列中加入條件節點,並進行相應的維護,當執行signal()或者signalAll()時,會將條件佇列中的節點維護到AQS中,進行佇列間的互動。

JAVA AQS 實現原理


相關文章