隊列同步器(AQS)詳解源碼分析

隊列同步器(AQS)簡介:

      AbstractQueueSynchronizer,用來構建鎖和其他同步組件的基礎框架,使用一個int型變量來表示同步狀態,通過內置的FIFO隊列來完成資源獲取線程的排隊工作。

       我們可以這麼理解,鎖是面向使用者的,即我們可以用鎖來完成多線程處理的一些問題,而隱藏了實現的細節,而同步器面向鎖的實現者,它簡化了鎖的實現方式,屏蔽了同步狀態管理,線程派對,等待與喚醒等底層的操作。相當於使用者使用鎖,而AQS來實現鎖。

使用場景:比如可重入鎖,CountDownLatch等鎖和工具類都用到了AQS

使用方式:

   子類繼承並實現他的抽象方法來管理同步狀態,對同步狀態進行修改時,使用同步器提供的3個方法(getState()、setState()、compareAndSetState())來操作,他們能保證狀態的改變是安全的。子類推薦爲自定義同步組件的靜態內部類。

剛纔提到的三個方法getState()、setState()、compareAndSetState()都是final方法,我們並不需要去重寫,需要重寫的方法是下面的這幾個方法:

當我們自定義同步組件時,將會調用同步器提供的模板方法,這些模板方法如下:

這些模板方法同樣是final的,我們在調用他們時,也會調用到我們之前重寫的方法,在後面我們會介紹到這些模板方法。

下面給出一個例子,大概瞭解一下怎麼使用(靜態內部類繼承AQS)

class Mutex implements Lock {
	// 靜態內部類, 自定義同步器
	private static class Sync extends AbstractQueuedSynchronizer {
		// 是否處於佔用狀態
		protected boolean isHeldExclusively() {
			return getState() == 1;
		}
		// 當狀態爲0的時候獲取鎖
		public boolean tryAcquire(int acquires) {
			if (compareAndSetState(0, 1) ) {
				setExclusiveOwnerThread(Thread. currentThread() ) ;
				return true;
			}
			return false;
		}
		// 釋放鎖, 將狀態設置爲0
		protected boolean tryRelease(int releases) {
			if (getState() == 0) throw new
					IllegalMonitorStateException() ;
			setExclusiveOwnerThread(null) ;
			setState(0) ;
			return true;
		}
		// 返回一個Condition, 每個condition都包含了一個condition隊列
		Condition newCondition() { return new ConditionObject() ; }
	}
	// 僅需要將操作代理到Sync上即可
	private final Sync sync = new Sync() ;
	public void lock() { sync. acquire(1) ; }
	public boolean tryLock() { return sync.tryAcquire(1) ; }
	public void unlock() { sync. release(1) ; }
	public Condition newCondition() { return sync.newCondition() ; }
	public boolean isLocked() { return sync. isHeldExclusively() ; }
	public boolean hasQueuedThreads() { return sync.hasQueuedThreads() ; }
	public void lockInterruptibly() throws InterruptedException {
		sync. acquireInterruptibly(1) ;
	}
	public boolean tryLock(long timeout, TimeUnit unit) throws InterruptedException {
		return sync. tryAcquireNanos(1, unit.toNanos(timeout) ) ;
	}
}

同步隊列:

      AQS中很重要的一個數據結構就是同步隊列了,他是一個FIFO雙向的同步隊列。

      當前的線程在獲取同步狀態失敗時,同步器會將當前線程以及等待狀態等信息構造成一個節點(Node)並將其加入同步隊列,同時會阻塞當前線程,當同步狀態釋放時,會把首節點中的線程喚醒,使其再次嘗試獲取同步狀態。

    Node節點是AQS中的一個內部類,成員如下

static final class Node {
        /** 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;

        /** waitStatus value to indicate thread has cancelled */
        static final int CANCELLED =  1;
        /** waitStatus value to indicate successor's thread needs unparking */
        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;

        volatile int waitStatus;

       
        volatile Node prev;

       
        volatile Node next;

       
        volatile Thread thread;

       
        Node nextWaiter;

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

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

        Node() {    // Used to establish initial head or SHARED marker
        }

        Node(Thread thread, Node mode) {     // Used by addWaiter
            this.nextWaiter = mode;
            this.thread = thread;
        }

        Node(Thread thread, int waitStatus) { // Used by Condition
            this.waitStatus = waitStatus;
            this.thread = thread;
        }
    }

而每個同步器中都會持有同步隊列的首節點和尾節點

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

所以基本結構如下:

剛纔說了獲取同步狀態失敗時,就會把線程信息加入一個新構建的節點,然後接入隊列的尾部,所以這個加入隊列的過程也必須要保證線程的安全,所以同步器有一個基於CAS的設置尾節點的方法:

compareAndSetTail(Node except,Node update)

首節點是獲取同步狀態成功的節點,首節點的線程在釋放同步狀態時,就會喚醒後續的節點,而後續的節點將會在獲取同步狀態成功時將自己設置爲首節點。設置首節點是由獲取同步狀態成功的線程來完成的,由於只有一個線程能夠成功的獲取到同步狀態,因此就不需要CAS來保證線程安全了(就只有一個線程)。

 

同步狀態的獲取與釋放:

       在瞭解了同步隊列的結構後,我們就可以來看看AQS到底是怎樣來進行同步狀態的獲取和釋放的。

       獲取與釋放分爲獨佔式的和共享式的。

      獨佔式:顧名思義就是同一時刻只能有一個線程獲取到鎖,其他獲取鎖線程只能處於同步隊列中等待,只有獲取鎖的線程釋放了鎖,後繼的線程才能夠獲取到鎖。

      共享式:同一時刻能夠有多個線程獲取到同步狀態。

(1)獨佔式同步狀態的獲取:

     通過acquire(int arg)方法獲取同步狀態,該方法對於中斷不敏感,也就是由於線程獲取同步狀態失敗後進入同步隊列中, 後續對線程進行中斷操作時, 線程不會從同步隊列中移出。

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

這個方法中調用了重寫之後tryAcquire(int arg)方法,還調用了addWaiter方法和acquireQueued方法

下面我們一個一個來分析一下這些方法

首先是addWaiter(Node node)方法

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

這個方法會使用compareAndSetTail()方法來吧當前線程加入尾節點,如果沒有加入成功,就會去調用enq()方法,那我們再看看enq(final Node node)方法

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

可以看出,enq方法就是一個死循環,而它所完成的工作和addWaiter(Node node)是一樣的,都是吧當前的線程加入到同步隊列的尾節點處,所以enq方法和addWaiter可以看做是同一個方法來對待,通過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);
        }
    }

同理,這還是一個死循環的方法,它的邏輯是判斷前驅節點是不是首結點(這個時候通過之前說的方法當前線程已經成功的加入了同步隊列),假如是首節點(只有他的前驅是首節點他纔有機會獲取同步狀態)那就嘗試獲取同步狀態,獲取成功的話就把自己設置爲首節點,否則就繼續循環直到他獲取成功爲止。

仔細看看這個方法是個無限循環,感覺如果p == head && tryAcquire(arg)條件不滿足循環將永遠無法結束,當然不會出現死循環,奧祕在於後面的parkAndCheckInterrupt會把當前線程掛起,從而阻塞住線程的調用棧。當然也不是馬上把請求不到鎖的線程進行阻塞,還要檢查該線程的狀態,比如如果該線程處於Cancel狀態則沒有必要,具體的檢查在shouldParkAfterFailedAcquire中:

private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
        int ws = pred.waitStatus;
        if (ws == Node.SIGNAL)           
            return true;
        if (ws > 0) {          
            do {
                node.prev = pred = pred.prev;
            } while (pred.waitStatus > 0);
            pred.next = node;
        } else {   
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
        }
        return false;
    }

檢查原則在於:

  • 規則1:如果前繼的節點狀態爲SIGNAL,表明當前節點需要unpark,則返回成功,此時acquireQueued方法的第12行(parkAndCheckInterrupt)將導致線程阻塞
  • 規則2:如果前繼節點狀態爲CANCELLED(ws>0),說明前置節點已經被放棄,則回溯到一個非取消的前繼節點,返回false,acquireQueued方法的無限循環將遞歸調用該方法,直至規則1返回true,導致線程阻塞
  • 規則3:如果前繼節點狀態爲非SIGNAL、非CANCELLED,則設置前繼的狀態爲SIGNAL,返回false後進入acquireQueued的無限循環,與規則2同

那麼我們現在總結一下,介紹了剛纔那麼多的方法,大多數的方法裏面都有一個死循環,這時回到最初的acquire(int arg)方法,我們就大概能夠明白這個方法的邏輯了,當獲取失敗時,那麼後面的方法就會把當前線程加入同步隊列,並且讓該節點一直自旋(相當於被阻塞了),直到他獲取到了同步狀態成功(別的線程釋放了)爲止。

那麼現在還有一個疑問,假如第一個獲取的線程在tryAcquire方法中就獲取同步狀態成功,直接返回ture,那麼線程就不會被包裝成節點加入到同步隊列中,那隊列何來的首節點和尾節點?

其實我們回去看addWaiter和enq方法就會發現這些方法都會去判斷是否有尾節點,當發現沒有尾節點時,就會在enq方法裏就會創建首節點和尾節點(首節點就是尾節點)

(代碼片段)

            Node t = tail;
            if (t == null) { // Must initialize
                if (compareAndSetHead(new Node()))
                    tail = head;
            } 

說了這麼多,對於鎖這種併發組件來說的話,從acquire(int arg)這個方法返回就代表當前線程獲取了鎖,以上就是這個方法的含義。

獲取的邏輯圖:

(2)獨佔式同步狀態的釋放

 剛纔講的是獲取,現在講的是如何釋放同步狀態。通過調用AQS的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;
    }

ps:喚醒的方法unparkSuccessor(Node node)使用了LockSupport工具類

 

(3)共享式同步狀態的獲取與釋放:

以文件的讀寫爲例,寫操作要求對資源的獨佔式訪問,而讀操作可以是共享式訪問。

一個線程在讀時,其他線程也可以讀,但是一個線程在寫時,其他線程均不能讀寫。

調用方法acquireShare(int arg)可以共享式獲取同步狀態,

 public final void acquireShared(int arg) {
        if (tryAcquireShared(arg) < 0)
            doAcquireShared(arg);
    }

當tryAcquireShared返回值大於等於0時,表示可以獲取到同步狀態,否則進入doAcquireShared(int arg)方法中自旋

private void doAcquireShared(int arg) {
        final Node node = addWaiter(Node.SHARED);
        boolean failed = true;
        try {
            boolean interrupted = false;
            for (;;) {
                final Node p = node.predecessor();
                if (p == head) {
                    int r = tryAcquireShared(arg);
                    if (r >= 0) {
                        setHeadAndPropagate(node, r);
                        p.next = null; // help GC
                        if (interrupted)
                            selfInterrupt();
                        failed = false;
                        return;
                    }
                }
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

同樣是一個死循環,不斷的進行tryAcquireShare(int arg)方法,直到獲取成功,其實它和獨佔式的差別不大,差別主要在setHeadAndPropagate方法,顧名思義,即在設置head之後多執行了一步propagate操作

private void setHeadAndPropagate(Node node, int propagate) {
        Node h = head; // Record old head for check below
        setHead(node);
       
        if (propagate > 0 || h == null || h.waitStatus < 0 ||
            (h = head) == null || h.waitStatus < 0) {
            Node s = node.next;
            if (s == null || s.isShared())
                doReleaseShared();
        }
    }

這意味着獨佔鎖某個節點被喚醒之後,它只需要將這個節點設置成head就完事了,而共享鎖不一樣,某個節點被設置爲head之後,如果它的後繼節點是SHARED狀態的,那麼將繼續通過doReleaseShared方法嘗試往後喚醒節點,實現了共享狀態的向後傳播。

釋放同步狀態使用releaseShare(int arg)方法

 public final boolean releaseShared(int arg) {
        if (tryReleaseShared(arg)) {
            doReleaseShared();
            return true;
        }
        return false;
    }

該方法在釋放同步狀態之後,將會喚醒後續處於等待狀態的節點。對於能夠支持多個線程同時訪問的併發組件(比如Semaphore),它和獨佔式主要區別在於tryReleaseShared(int arg)方法必須確保同步狀態(或者資源數)線程安全釋放,一般是通過循環和CAS來保證的, 因爲釋放同步狀態的操作會同時來自多個線程。

(4)獨佔式超時獲取同步狀態

      先介紹一下響應中斷的同步狀態獲取過程。在Java 5之前,當一個線程獲取不到鎖而被阻塞在synchronized之外時,對該線程進行中斷操作,此時該線程的中斷標誌位會被修改, 但線程依舊會阻塞在synchronized上,等待着獲取鎖。在Java 5中,同步器提供了acquireInterruptibly(int arg)方法, 這個方法在等待獲取同步狀態時, 如果當前線程被中斷, 會立刻返回, 並拋出InterruptedException。超時獲取同步狀態過程可以被視作響應中斷獲取同步狀態過程的“增強版”,doAcquireNanos(int arg,long nanosTimeout)方法在支持響應中斷的基礎上, 增加了超時獲取的特性。 針對超時獲取, 主要需要計算出需要睡眠的時間間隔nanosTimeout, 爲了防止過早通知,nanosTimeout計算公式爲: nanosTimeout-=now-lastTime, 其中now爲當前喚醒時間, lastTime爲上次喚醒時間, 如果nanosTimeout大於0則表示超時時間未到, 需要繼續睡眠nanosTimeout納秒,反之, 表示已經超時

private boolean doAcquireNanos(int arg, long nanosTimeout)
            throws InterruptedException {
        if (nanosTimeout <= 0L)
            return false;
        final long deadline = System.nanoTime() + nanosTimeout;
        final Node node = addWaiter(Node.EXCLUSIVE);
        boolean failed = true;
        try {
            for (;;) {
                final Node p = node.predecessor();
                if (p == head && tryAcquire(arg)) {
                    setHead(node);
                    p.next = null; // help GC
                    failed = false;
                    return true;
                }
                nanosTimeout = deadline - System.nanoTime();
                if (nanosTimeout <= 0L)
                    return false;
                if (shouldParkAfterFailedAcquire(p, node) &&
                    nanosTimeout > spinForTimeoutThreshold)
                    LockSupport.parkNanos(this, nanosTimeout);
                if (Thread.interrupted())
                    throw new InterruptedException();
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

該方法在自旋過程中,當節點的前驅節點爲頭節點時嘗試獲取同步狀態,如果獲取成功則從該方法返回,這個過程和獨佔式同步獲取的過程類似,但是在同步狀態獲取失敗的處理上有所不同。如果當前線程獲取同步狀態失敗,則判斷是否超時(nanosTimeout小於等於0表示已經超時),如果沒有超時,重新計算超時間隔nanosTimeout,然後使當前線程等待nanosTimeout納秒(當已到設置的超時時間,該線程會從LockSupport.parkNanos(Object blocker,long nanos)方法返回)。如果nanosTimeout小於等於spinForTimeoutThreshold(1000納秒)時,將不會使該線程進行超時等待,而是進入快速的自 旋過程。 原因在於,非常短的超時等待無法做到十分精確, 如果這時再進行超時等待, 相反會讓nanosTimeout的超時從整體上表現得反而不精確。因此,在超時非常短的場景下, 同步器會進入無條件的快速自旋。

 

使用的例子

最後給出一個使用的例子——TwinsLock,這個工具類允許在同一時刻,之多兩個線程同時訪問。

import java.util.concurrent.locks.AbstractQueuedSynchronizer;
import java.util.concurrent.locks.Lock;

public class TwinsLock implements Lock {
	private final Sync sync = new Sync(2) ;
	private static final class Sync extends AbstractQueuedSynchronizer {
		Sync(int count) {
			if (count <= 0) {
				throw new IllegalArgumentException("count must large than zero.");
			}
			setState(count) ;
		}
		public int tryAcquireShared(int reduceCount) {
			for (; ; ) {
				int current = getState() ;
				int newCount = current - reduceCount;
				if (newCount < 0 || compareAndSetState(current,
						newCount) ) {
					return newCount;
				}
			}
		}
		public boolean tryReleaseShared(int returnCount) {
			for (; ; ) {
				int current = getState() ;
				int newCount = current + returnCount;
				if (compareAndSetState(current, newCount) ) {
					return true;
				}
			}
		}
	}
	public void lock() {
		sync. acquireShared(1) ;
	}
	public void unlock() {
		sync. releaseShared(1) ;
	}
	// 其他接口方法略
}

 

 

 

 

 

 

 

 

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