詳解AQS

一、什麼是AQS

我們常用的j.u.c包裏,提供了許多強大的同步工具,例如ReentrantLock,Semphore,ReentrantReadWriteLock等,但當這些工具難以滿足某個場景的需求時,我們就需要定製化我們自己的同步器,這時,我們可能會想,如果能有一個像Servlet這種只要重寫某幾個方法就能完成一把定製鎖的實現的就好了!! 沒錯,AQS就是提供了這樣一種功能,它如果要實現一個同步器的大部分通用功能都幫我們實現好了,然後提供出抽象函數供我們重寫來定製化自己想要的同步器。 實際上,上面所說的ReentrantLock,Semphore,ReentrantReadWriteLock等juc包中同步工具的實現,也都是在AQS的輔助下進行的“二次開發”。 例如在ReentrantLock繼承了Lock接口,然後利用定製化了的繼承了AQS的類,來去實現Lock接口。


二、AQS提供了什麼功能

同步器一般會包括兩種方法,一種是acquire方法, 另一種是release方法; acquire方法是嘗試獲取鎖操作,如果獲取不到就阻塞(park)當前線程,並將其放入等待隊列中;release方法是釋放鎖操作,然後會從等待隊列中出隊一個或多個被acquire阻塞的線程並將其喚醒(unpark).

j.u.c包中並沒有對同步器的API做一個統一的定義。因此,有一些類定義了通用的接口(如Lock),而另外一些則定義了其專有的版本。因此在不同的類中,acquire和release操作的名字和形式會各有不同。例如:Lock.lock,Semaphore.acquire,CountDownLatch.await和FutureTask.get,在這個框架裏,這些方法都是acquire操作。但是,J.U.C爲支持一系列常見的使用選項,在類間都有個一致約定。在有意義的情況下,每一個同步器都支持下面的操作:

  • 阻塞(例如:acquire)和非阻塞(例如:tryAcquire)同步。
  • 可選的超時設置,讓調用者可以放棄等待
  • 通過中斷實現的任務取消,通常是分爲兩個版本,一個acquire可取消,而另一個不可以(例如ReentrantLock中的lockInterruptibly()就是可在阻塞等待中被中斷的,而lock()是阻塞等待中不可被中斷的)。

三、讀源碼之前需要知道的知識

AQS的內部隊列

在AQS中,被阻塞的線程會被打包成一個Node然後放到等待隊列中,head指向隊列頭結點,tail指向尾結點,隊列不存在時(未初始化時)的樣子爲:head==tail==null ,初始化之後,隊列爲空的情況爲:head==tail==dummy頭結點,如下圖所示:
在這裏插入圖片描述
head指向dummy頭結點,這個頭結點存在的意義是爲了方便隊列操作,並且裏面保存的thread恆爲null。下面來看一下node每個字段的意思

Node

爲了抓住重點學習,這裏只介紹Node裏的重要成員:

  • thread :當前結點裏保存的線程
  • prev,next:當前結點的前後指針,這裏隊列的實現是帶有頭結點的雙向鏈表。 prev是靠近頭結點那一端的,next是靠近尾結點那一端的。
  • waitStatus:初始狀態爲0。爲-1時,表示存在正在阻塞等待的線程,結點入隊之後,會自旋一次來再次嘗試tryAcquire,如果依然失敗,纔會進入阻塞,自旋的這一次就是把waitStatus字段CAS成-1。 這一字段取值範圍如下:
/** waitStatus value to indicate thread has cancelled */
static final int CANCELLED =  1;

// 當前結點爲-1, 則說明後一個結點需要park阻塞
static final int SIGNAL    = -1;

/** waitStatus value to indicate thread is waiting on condition */
static final int CONDITION = -2;

/**
 * waitStatus value to indicate the next acquireShared should
 * unconditionally propagate
 */
static final int PROPAGATE = -3;

四、AQS源碼解讀

這裏先更新一下獨佔式的部分。。共享式的日後再看.

一、獨佔式代碼部分

先有個宏觀上的理解,如下圖:
在這裏插入圖片描述
其中tryRelease,tryAcquire是非阻塞式獲取鎖。 有了宏觀上的框架,再去看一下實現的細節。

1. acquire

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

這裏使用了短路原理, 如果tryAcquire成功的話,就直接跳出if了; 如果 tryAcquire失敗,那麼會先執行addWaiter把當前線程打包成一個node放入等待隊列, 然後再執行acquireQueued嘗試一次自旋,如果依然無法獲取到鎖,就進入阻塞。

2. addWaiter

private Node addWaiter(Node mode) {
     Node node = new Node(Thread.currentThread(), mode);
     // Try the fast path of enq; backup to full enq on failure
     Node pred = tail;
     if (pred != null) {
         node.prev = pred;
         if (compareAndSetTail(pred, node)) {
             pred.next = node;
             return node;
         }
     }
     enq(node);
     return node;
 }

將當前線程打包成一個node, 然後將這個node入隊,如果入隊失敗則有2種情況:

  • 隊列還不存在(隊列還沒初始化)
  • 在入隊時,出現了同步問題。(這裏的隊列也是臨界資源,如果CAS失敗說明資源競爭失敗)
    當入隊失敗時,進入enq函數,這一函數的作用是:初始化隊列並自旋入隊操作。

3. enq

 private Node enq(final Node node) {
     for (;;) {
         Node t = 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;
             }
         }
     }
 }

如果隊列未初始化,那麼就初始化隊列,如果已經初始化了,就將當前結點自旋入隊,該方法一定返回true.

線程被打包成結點,然後入隊之後,會進入acquireQueued進行一次自旋try,如果依然失敗就阻塞

4. acquireQueued

final boolean acquireQueued(final Node node, int arg) {
    booleanfailed = 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);
    }
}

先判斷前驅結點是不是head,因爲head指向的是dummy結點,因此,如果前驅結點就是head了,那麼當前結點就是隊首了!! 然後只有隊首的結點纔有資格在第一次自旋的時候進行tryAcquire

每一個結點不會改變自己的waitStatus, 只會改變在隊列中前驅結點的waitStatus , 因此,如果前驅結點是0,則通過CAS操作將其變爲-1,然後自旋一次,如果前驅結點是-1,則說明已經自旋過一次了,然後才能進入 parkAndCheckInterrupt函數,也就是將當前結點的線程阻塞。

這個函數裏的幾個細節,如果隊首元素成功tryAcquire,則需要進行出隊操作,把當前結點設置成dummy結點就可以了。
在setHead的時候。 會將thread設置成null 也是用於help gc 。 同時也要手動讓前驅結點的next設置爲null, 方便gc回收…

到此位置,線程就會被卡在parkAndCheckInterrupt這個函數中,等待被喚醒

5. release

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

release的實現就更短了,如果tryRelease成功的話,就看是否還存在阻塞等待的線程,if (h != null && h.waitStatus != 0) 這句話的判斷就是判斷否還存在阻塞等待的線程。 如果h是null的話,則說明隊列根本就不存在,更別說等待的線程了,如果h.waitStatus不是0的話,則說明隊列裏存在等待的線程node。

如果存在正在等待的線程的話,就unparkSuccessor , 即喚醒這個正在等待的隊首線程.

6. unparkSuccessor

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

其中,s是下一個需要被喚醒的node結點,然後後面會對其進行unpark(喚醒)操作。


五、AQS的使用

到目前位置,只是簡單過完了一遍AQS的獨佔式的acquire和release操作, 它幫我們完成了一部分同步狀態管理事情,但是最關鍵的tryAcquiretryRelease 其實它是一個需要我們去重寫的方法:

protected boolean tryAcquire(int arg) {
    throw new UnsupportedOperationException();
}

一、需要做的事情

在使用AQS的時候,往往需要我們自己去重寫:

  • tryAcquire
  • tryRelease
  • tryAcquireShared
  • tryReleaseShared
  • isHeldExclusively:如果對於當前(正調用的)線程,同步是以獨佔方式進行的,則返回 true。此方法只是 abstractqueuedsynchronizer.conditionobject 方法內進行內部調用,因此,如果不使用條件,則不需要定義它。

在實現tryAcquire的時候,我們需要對內部的status進行操作,AQS也提供給了我們關於Status操作接口,分別是:

  • getState()
  • setState(int)
  • compareAndSetState(int, int)

源碼實現如下:

protected final int getState() {
    return state;
}
    
protected final void setState(int newState) {
state = newState;
}
    
protected final boolean compareAndSetState(int expect, int update) {
// See below for intrinsics setup to support this
return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}

AQS在使用的時候,往往是使用一個內部類繼承AQS,然後重寫上述提到的方法,然後就可以在當前類中使用這個內部類的acquire / release來實現同步了

二、使用AQS完成信號量的功能

class Mutex implements Lock, java.io.Serializable {
    // 自定義同步器
    private static class Sync extends AbstractQueuedSynchronizer {
        // 判斷是否鎖定狀態
        protected boolean isHeldExclusively() {
            return getState() == 1;
        }

        // 嘗試獲取資源,立即返回。成功則返回true,否則false。
        public boolean tryAcquire(int acquires) {
            assert acquires == 1; // 這裏限定只能爲1個量
            if (compareAndSetState(0, 1)) {//state爲0才設置爲1,不可重入!
                setExclusiveOwnerThread(Thread.currentThread());//設置爲當前線程獨佔資源
                return true;
            }
            return false;
        }

        // 嘗試釋放資源,立即返回。成功則爲true,否則false。
        protected boolean tryRelease(int releases) {
            assert releases == 1; // 限定爲1個量
            if (getState() == 0)//既然來釋放,那肯定就是已佔有狀態了。只是爲了保險,多層判斷!
                throw new IllegalMonitorStateException();
            setExclusiveOwnerThread(null);
            setState(0);//釋放資源,放棄佔有狀態
            return true;
        }
    }

    // 真正同步類的實現都依賴繼承於AQS的自定義同步器!
    private final Sync sync = new Sync();

    //lock<-->acquire。兩者語義一樣:獲取資源,即便等待,直到成功才返回。
    public void lock() {
        sync.acquire(1);
    }

    //tryLock<-->tryAcquire。兩者語義一樣:嘗試獲取資源,要求立即返回。成功則爲true,失敗則爲false。
    public boolean tryLock() {
        return sync.tryAcquire(1);
    }

    //unlock<-->release。兩者語文一樣:釋放資源。
    public void unlock() {
        sync.release(1);
    }

    //鎖是否佔有狀態
    public boolean isLocked() {
        return sync.isHeldExclusively();
    }
}

瞭解了AQS的原理之後,可以來趁熱打鐵的看一下ReentrantLock的加鎖實現

六、ReentrantLock的原理

這裏主要詳細介紹一下ReentrantLock對AQS的兩種實現方式:

  • 公平鎖(FairSync)
  • 非公平鎖(NonfairSync)
    在這裏插入圖片描述
    其中Sync是公平鎖和非公平鎖的抽象基類,裏面已經初步實現了一些方法,但其中的lock()方法和tryAcquire()方法依然是抽象的,需要子類去進行實現,而公平鎖和非公平鎖的主要區別也主要在這兩個函數中,下面來看一下。

公平鎖與非公平鎖的實現區別

1. lock操作:

公平鎖

final void lock() {
	if (compareAndSetState(0, 1))
	      setExclusiveOwnerThread(Thread.currentThread());
	else
	    acquire(1);
}

非公平鎖

final void lock() {
	acquire(1);
}

可以看到,非公平鎖在lock的時候會進行一次CAS操作,如果直接獲取到鎖了的話,那麼就直接繼續執行。 在臨界區的執行速度比較快的情況下,非公平鎖會比公平鎖要更快,因爲在喚醒阻塞線程的過程中,有可能有其他線程已經取得鎖然後執行完並釋放了。。

2. tryAcquire操作:

非公平鎖:

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

final boolean nonfairTryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();
    if (c == 0) {
    	// 這裏直接進行CAS , 嘗試拿鎖
        if (compareAndSetState(0, acquires)) {
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    else if (current == getExclusiveOwnerThread()) {
        // 重入時,給state加一個acquires偏移量,對應release時會減去一次
        int nextc = c + acquires;
        if (nextc < 0) // overflow
            throw new Error("Maximum lock count exceeded");
        setState(nextc);
        return true;
    }
    return false;
}

公平鎖

protected final boolean tryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();
    if (c == 0) {
    	// 這裏會先判斷是否存在比當前線程等待更久的線程!
    	// 只有不存在等待的線程的時候,纔有資格去嘗試獲取鎖資源(CAS)
        if (!hasQueuedPredecessors() &&
            compareAndSetState(0, acquires)) {
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    else if (current == getExclusiveOwnerThread()) {
        // 重入時,給state加一個acquires偏移量,對應release時會減去一次
        int nextc = c + acquires;
        if (nextc < 0)
            throw new Error("Maximum lock count exceeded");
        setState(nextc);
        return true;
    }
    return false;
}

可以看出,在tryAcquire時,公平鎖會先判斷是否存在比當前線程等待的更久的線程,如果不存在這樣的線程,才能進行CAS嘗試獲取鎖; 而非公平鎖是直接進行CAS獲取鎖。

關於Interrupt

我們知道, thread1.interrupt()就是將thread1的中斷標誌位置爲1(Thread.interrupted()是檢測並清除中斷標誌,thread1.isInterrupted()是僅僅檢測thread1的中斷標誌但不清除).

ReentrantLock()lock()方法,thread因等待資源而被阻塞在等待隊列中的時候,不會被打斷,而是先將這個中斷標記位記下來,然後當獲取到鎖資源之後,執行selfInterrupt(), 也就是在獲得鎖資源後打斷自己!! 如果希望在阻塞隊列中依然可以被打斷的話,應該使用lockInterruptibly , 這個lock操作是可以允許線程在阻塞等待時被中斷的!


到此爲止,我們看到了在ReentrantLock中對tryAcquire和tryRelease的實現,分別實現了公平競爭和非公平競爭的場景,因爲這裏的ReentrantLock是獨佔式的鎖(也就是說資源只允許被一個線程獲取,也可以理解成01信號量),所以並沒有實現 tryAcquireSharedtryReleaseShared 這兩個方法。 實際上,我們在使用的時候也是,需要哪種模式就實現對應模式的acquire和release.

對於 tryAcquireSharedtryReleaseShared 這兩個方法的實現例子,可以去看看Semphore的源碼,它就是隻重寫了tryAcquireSharedtryReleaseShared,理解完上面分析的代碼之後,去看Semphore的源碼也不會很困難了。。日後有時間再寫Semphore的源碼記錄把。。

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