Java併發編程-看懂AQS的前世今生

来源:https://www.cnblogs.com/iou123lg/archive/2018/08/12/9464385.html
-Advertisement-
Play Games

在具備了volatile、CAS和模板方法設計模式的知識之後,我們可以來深入學習下AbstractQueuedSynchronizer(AQS),本文主要想從AQS的產生背景、設計和結構、源代碼實現及AQS應用這4個方面來學習下AQS,文章耗時一個月,所以篇幅有點長,需要一點耐心。 1、AQS產生背 ...


  在具備了volatileCAS模板方法設計模式的知識之後,我們可以來深入學習下AbstractQueuedSynchronizer(AQS),本文主要想從AQS的產生背景、設計和結構、源代碼實現及AQS應用這4個方面來學習下AQS,文章耗時一個月,所以篇幅有點長,需要一點耐心。

  1、AQS產生背景

  通過JCP的JSR166規範,Jdk1.5開始引入了j.u.c包,這個包提供了一系列支持併發的組件。這些組件是一系列的同步器,這些同步器主要維護著以下幾個功能:內部同步狀態的管理(例如表示一個鎖的狀態是獲取還是釋放),同步狀態的更新和檢查操作,且至少有一個方法會導致調用線程在同步狀態被獲取時阻塞,以及在其他線程改變這個同步狀態時解除線程的阻塞。上述的這些的實際例子包括:互斥排它鎖的不同形式、讀寫鎖、信號量、屏障、Future、事件指示器以及傳送隊列等。可以看下這裡的4.2的圖便能理解j.u.c包的組件構成。

  幾乎任一同步器都可以用來實現其他形式的同步器。例如,可以用可重入鎖實現信號量或者用信號量實現可重入鎖。但是,這樣做帶來的複雜性、開銷及不靈活使j.u.c最多只能是一個二流工程,且缺乏吸引力。如果任何這樣的構造方式不能在本質上比其他形式更簡潔,那麼開發者就不應該隨意地選擇其中的某個來構建另一個同步器。因此,JSR166基於AQS類建立了一個小框架,這個框架為構造同步器提供一種通用的機制,並且被j.u.c包中大部分類使用,同時很多用戶也可以用它來定義自己的同步器。這個就是j.u.c的作者Doug Lea大神的初衷,通過提供AQS這個基礎組件來構建j.u.c的各種工具類,至此就可以理解AQS的產生背景了。

  2、AQS的設計和結構

  2.1 設計思想

  同步器的核心方法是acquire和release操作,其背後的思想也比較簡潔明確。acquire操作是這樣的:

  while (當前同步器的狀態不允許獲取操作) {

          如果當前線程不在隊列中,則將其插入隊列

          阻塞當前線程

  }

  如果線程位於隊列中,則將其移出隊列

  

  release操作是這樣的:

  更新同步器的狀態

  if (新的狀態允許某個被阻塞的線程獲取成功)

           解除隊列中一個或多個線程阻塞狀態

  從這兩個操作中的思想中我們可以提取出三大關鍵操作:同步器的狀態變更、線程阻塞和釋放、插入和移出隊列。所以為了實現這兩個操作,需要協調三大關鍵操作引申出來的三個基本組件:

  ·同步器狀態的原子性管理;

  ·線程阻塞與解除阻塞;

  ·隊列的管理;

  由這三個基本組件,我們來看j.u.c是怎麼設計的。

  2.1.1 同步狀態

  AQS類使用單個int(32位)來保存同步狀態,並暴露出getState、setState以及compareAndSet操作來讀取和更新這個同步狀態。其中屬性state被聲明為volatile,並且通過使用CAS指令來實現compareAndSetState,使得當且僅當同步狀態擁有一個一致的期望值的時候,才會被原子地設置成新值,這樣就達到了同步狀態的原子性管理,確保了同步狀態的原子性、可見性和有序性。

   基於AQS的具體實現類(如鎖、信號量等)必鬚根據暴露出的狀態相關的方法定義tryAcquire和tryRelease方法,以控制acquire和release操作。當同步狀態滿足時,tryAcquire方法必須返回true,而當新的同步狀態允許後續acquire時,tryRelease方法也必須返回true。這些方法都接受一個int類型的參數用於傳遞想要的狀態。

  2.1.2 阻塞

  直到JSR166,阻塞線程和解除線程阻塞都是基於Java的內置管程,沒有其它非基於Java內置管程的API可以用來達到阻塞線程和解除線程阻塞。唯一可以選擇的是Thread.suspend和Thread.resume,但是它們都有無法解決的競態問題,所以也沒法用,目前該方法基本已被拋棄。具體不能用的原因可以官方給出的答覆

  j.u.c.locks包提供了LockSupport類來解決這個問題。方法LockSupport.park阻塞當前線程直到有個LockSupport.unpark方法被調用。unpark的調用是沒有被計數的,因此在一個park調用前多次調用unpark方法只會解除一個park操作。另外,它們作用於每個線程而不是每個同步器。一個線程在一個新的同步器上調用park操作可能會立即返回,因為在此之前可以有多餘的unpark操作。但是,在缺少一個unpark操作時,下一次調用park就會阻塞。雖然可以顯式地取消多餘的unpark調用,但並不值得這樣做。在需要的時候多次調用park會更高效。park方法同樣支持可選的相對或絕對的超時設置,以及與JVM的Thread.interrupt結合 ,可通過中斷來unpark一個線程。

  2.1.3 隊列

  整個框架的核心就是如何管理線程阻塞隊列,該隊列是嚴格的FIFO隊列,因此不支持線程優先順序的同步。同步隊列的最佳選擇是自身沒有使用底層鎖來構造的非阻塞數據結構,業界主要有兩種選擇,一種是MCS鎖,另一種是CLH鎖。其中CLH一般用於自旋,但是相比MCS,CLH更容易實現取消和超時,所以同步隊列選擇了CLH作為實現的基礎。

  CLH隊列實際並不那麼像隊列,它的出隊和入隊與實際的業務使用場景密切相關。它是一個鏈表隊列,通過AQS的兩個欄位head(頭節點)和tail(尾節點)來存取,這兩個欄位是volatile類型,初始化的時候都指向了一個空節點。如下圖:

  入隊操作:CLH隊列是FIFO隊列,故新的節點到來的時候,是要插入到當前隊列的尾節點之後。試想一下,當一個線程成功地獲取了同步狀態,其他線程將無法獲取到同步狀態,轉而被構造成為節點並加入到同步隊列中,而這個加入隊列的過程必須要保證線程安全,因此同步器提供了一個CAS方法,它需要傳遞當前線程“認為”的尾節點和當前節點,只有設置成功後,當前節點才正式與之前的尾節點建立關聯。入隊操作示意圖大致如下:

  出隊操作:因為遵循FIFO規則,所以能成功獲取到AQS同步狀態的必定是首節點,首節點的線程在釋放同步狀態時,會喚醒後續節點,而後續節點會在獲取AQS同步狀態成功的時候將自己設置為首節點。設置首節點是由獲取同步成功的線程來完成的,由於只能有一個線程可以獲取到同步狀態,所以設置首節點的方法不需要像入隊這樣的CAS操作,只需要將首節點設置為原首節點的後續節點同時斷開原節點、後續節點的引用即可。出隊操作示意圖大致如下:

  這一小節只是簡單的描述了隊列的大概,目的是為了表達清楚隊列的設計框架,實際上CLH隊列已經和初始的CLH隊列已經發生了一些變化,具體的可以看查看資料中Doug Lea的那篇論文中的3.3 Queues。

  2.1.4 條件隊列

  上一節的隊列其實是AQS的同步隊列,這一節的隊列是條件隊列,隊列的管理除了有同步隊列,還有條件隊列。AQS只有一個同步隊列,但是可以有多個條件隊列。AQS框架提供了一個ConditionObject類,給維護獨占同步的類以及實現Lock介面的類使用。

  ConditionObject類實現了Condition介面,Condition介面提供了類似Object管程式的方法,如await、signal和signalAll操作,還擴展了帶有超時、檢測和監控的方法。ConditionObject類有效地將條件與其它同步操作結合到了一起。該類只支持Java風格的管程訪問規則,這些規則中,當且僅噹噹前線程持有鎖且要操作的條件(condition)屬於該鎖時,條件操作才是合法的。這樣,一個ConditionObject關聯到一個ReentrantLock上就表現的跟內置的管程(通過Object.wait等)一樣了。兩者的不同僅僅在於方法的名稱、額外的功能以及用戶可以為每個鎖聲明多個條件。

  ConditionObject類和AQS共用了內部節點,有自己單獨的條件隊列。signal操作是通過將節點從條件隊列轉移到同步隊列中來實現的,沒有必要在需要喚醒的線程重新獲取到鎖之前將其喚醒。signal操作大致示意圖如下:

  await操作就是當前線程節點從同步隊列進入條件隊列進行等待,大致示意圖如下:

  實現這些操作主要複雜在,因超時或Thread.interrupt導致取消了條件等待時,該如何處理。await和signal幾乎同時發生就會有競態問題,最終的結果遵照內置管程相關的規範。JSR133修訂以後,就要求如果中斷發生在signal操作之前,await方法必須在重新獲取到鎖後,拋出InterruptedException。但是,如果中斷發生在signal後,await必須返回且不拋異常,同時設置線程的中斷狀態。

  2.2 方法結構

  如果我們理解了上一節的設計思路,我們大致就能知道AQS的主要數據結構了。

組件

數據結構

同步狀態

volatile int state

阻塞

LockSupport類

隊列

Node節點

條件隊列

ConditionObject

  進而再來看下AQS的主要方法及其作用。

屬性、方法

描述、作用

int getState()

獲取當前同步狀態

void setState(int newState)

設置當前同步狀態

boolean compareAndSetState(int expect, int update)

通過CAS設置當前狀態,此方法保證狀態設置的原子性

boolean tryAcquire(int arg)

鉤子方法,獨占式獲取同步狀態,AQS沒有具體實現,具體實現都在子類中,實現此方法需要查詢當前同步狀態並判斷同步狀態是否符合預期,然後再CAS設置同步狀態

boolean tryRelease(int arg)

鉤子方法,獨占式釋放同步狀態,AQS沒有具體實現,具體實現都在子類中,等待獲取同步狀態的線程將有機會獲取同步狀態

int tryAcquireShared(int arg)

鉤子方法,共用式獲取同步狀態,AQS沒有具體實現,具體實現都在子類中,返回大於等於0的值表示獲取成功,反之失敗

boolean tryReleaseShared(int arg)

鉤子方法,共用式釋放同步狀態,AQS沒有具體實現,具體實現都在子類中

boolean isHeldExclusively()

鉤子方法,AQS沒有具體實現,具體實現都在子類中,當前同步器是否在獨占模式下被線程占用,一般該方法表示是否被當前線程所獨占

void acquire(int arg)

模板方法,獨占式獲取同步狀態,如果當前線程獲取同步狀態成功,則由該方法返回,否則會進入同步隊列等待,此方法會調用子類重寫的tryAcquire方法

void acquireInterruptibly(int arg)

模板方法,與acquire相同,但是此方法可以響應中斷,當前線程未獲取到同步狀態而進入同步隊列中,如果當前線程被中斷,此方法會拋出InterruptedException並返回

boolean tryAcquireNanos(int arg, long nanosTimeout)

模板方法,在acquireInterruptibly基礎上增加了超時限制,如果當前線程在超時時間內沒有獲取到同步狀態,則會返回false,如果獲取到了則會返回true

boolean release(int arg)

模板方法,獨占式的釋放同步狀態,該方法會在釋放同步狀態後,將同步隊列中的第一個節點包含的線程喚醒

void acquireShared(int arg)

模板方法,共用式的獲取同步狀態,如果當前系統未獲取到同步狀態,將會進入同步隊列等待,與acquire的主要區別在於同一時刻可以有多個線程獲取到同步狀態

void acquireSharedInterruptibly(int arg)

模板方法,與acquireShared一致,但是可以響應中斷

boolean tryAcquireSharedNanos(int arg, long nanosTimeout)

模板方法,在acquireSharedInterruptibly基礎上增加了超時限制

boolean releaseShared(int arg)

模板方法,共用式的釋放同步狀態

Collection<Thread> getQueuedThreads()

模板方法,獲取等待在同步隊列上的線程集合

Node int waitStatus

等待狀態

1、 CANCELLED,值為1,在同步隊列中等待的線程等待超時或者被中斷,需要從同步隊列中取消等待,節點進入該狀態後將不會變化;

2、 SIGNAL,值為-1,後續節點的線程處於等待狀態,而當前節點的線程如果釋放了同步狀態或者被取消,將會通知後續節點,使後續節點的線程得以運行;

3、 CONDITION,值為-2,節點在條件隊列中,節點線程等待在Condition上,當其他線程對Condition調用了signal()方法後,該節點將會從條件隊列中轉移到同步隊列中,加入到對同步狀態的獲取中;

4、 PROPAGATE,值為-3,表示下一次共用式同步狀態獲取將會無條件地傳播下去

Node prev

前驅節點,當節點加入同步隊列時被設置

Node next

後續節點

Thread thread

獲取同步狀態的線程

Node nextWaiter

條件隊列中的後續節點,如果當前節點是共用的,那麼這個欄位將是一個SHARED變數,也就是說節點類型(獨占和共用)和條件隊列中的後續節點共用同一個欄位

LockSupport void park()

阻塞當前線程,如果調用unpark方法或者當前線程被中斷,才能從park方法返回

LockSupport void unpark(Thread thread)

喚醒處於阻塞狀態的線程

ConditionObject Node firstWaiter

條件隊列首節點

ConditionObject Node lastWaiter

條件隊列尾節點

void await()

當前線程進入等待狀態直到signal或中斷,當前線程將進入運行狀態且從await方法返回的情況,包括:

其他線程調用該Condition的signal或者signalAll方法,且當前線程被選中喚醒;

其他線程調用interrupt方法中斷當前線程;

如果當前線程從await方法返回表明該線程已經獲取了Condition對象對應的鎖

void awaitUninterruptibly()

和await方法類似,但是對中斷不敏感

long awaitNanos(long nanosTimeout)

當前線程進入等待狀態直到被signal、中斷或者超時。返回值表示剩餘的時間。

boolean awaitUntil(Date deadline)

當前線程進入等待狀態直到被signal、中斷或者某個時間。如果沒有到指定時間就被通知,方法返回true,否則表示到了指定時間,返回false

void signal()

喚醒一個等待在Condition上的線程,該線程從等待方法返回前必須獲得與Condition相關聯的鎖

void signalAll()

喚醒所有等待在Condition上的線程,能夠從等待方法返回的線程必須獲得與Condition相關聯的鎖

  看到這,我們對AQS的數據結構應該基本上有一個大致的認識,有了這個基本面的認識,我們就可以來看下AQS的源代碼。

  3、AQS的源代碼實現

  主要通過獨占式同步狀態的獲取和釋放、共用式同步狀態的獲取和釋放來看下AQS是如何實現的。

  3.1 獨占式同步狀態的獲取和釋放

  獨占式同步狀態調用的方法是acquire,代碼如下:

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

  上述代碼主要完成了同步狀態獲取、節點構造、加入同步隊列以及在同步隊列中自旋等待的相關工作,其主要邏輯是:首先調用子類實現的tryAcquire方法,該方法保證線程安全的獲取同步狀態,如果同步狀態獲取失敗,則構造獨占式同步節點(同一時刻只能有一個線程成功獲取同步狀態)並通過addWaiter方法將該節點加入到同步隊列的尾部,最後調用acquireQueued方法,使得該節點以自旋的方式獲取同步狀態。如果獲取不到則阻塞節點中的線程,而被阻塞線程的喚醒主要依靠前驅節點的出隊或阻塞線程被中斷來實現。

  下麵來首先來看下節點構造和加入同步隊列是如何實現的。代碼如下:

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
        // 嘗試快速在尾節點後新增節點 提升演算法效率 先將尾節點指向pred
        Node pred = tail;
        if (pred != null) {
            //尾節點不為空  當前線程節點的前驅節點指向尾節點
            node.prev = pred;
            //併發處理 尾節點有可能已經不是之前的節點 所以需要CAS更新
            if (compareAndSetTail(pred, node)) {
                //CAS更新成功 當前線程為尾節點 原先尾節點的後續節點就是當前節點
                pred.next = node;
                return node;
            }
        }
        //第一個入隊的節點或者是尾節點後續節點新增失敗時進入enq
        enq(node);
        return 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;
                }
            }
        }
    }

  節點進入同步隊列之後,就進入了一個自旋的過程,每個線程節點都在自省地觀察,當條件滿足,獲取到了同步狀態,就可以從這個自旋過程中退出,否則依舊留在這個自旋過程中並會阻塞節點的線程,代碼如下:

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

  再來看看shouldParkAfterFailedAcquire和parkAndCheckInterrupt是怎麼來阻塞當前線程的,代碼如下:

private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
        //前驅節點的狀態決定後續節點的行為
     int ws = pred.waitStatus;
        if (ws == Node.SIGNAL)
            /*前驅節點為-1 後續節點可以被阻塞
             * This node has already set status asking a release
             * to signal it, so it can safely park.
             */
            return true;
        if (ws > 0) {
            /*
             * Predecessor was cancelled. Skip over predecessors and
             * indicate retry.
             */
            do {
                node.prev = pred = pred.prev;
            } while (pred.waitStatus > 0);
            pred.next = node;
        } else {
            /*前驅節點是初始或者共用狀態就設置為-1 使後續節點阻塞
             * 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.
             */
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
        }
        return false;
    }
private final boolean parkAndCheckInterrupt() {
        //阻塞線程
        LockSupport.park(this);
        return Thread.interrupted();
    }

  節點自旋的過程大致示意圖如下,其實就是對圖二、圖三的補充。

                圖六  節點自旋獲取隊列同步狀態

  整個獨占式獲取同步狀態的流程圖大致如下:

            圖七  獨占式獲取同步狀態

  當同步狀態獲取成功之後,當前線程從acquire方法返回,對於鎖這種併發組件而言,就意味著當前線程獲取了鎖。有獲取同步狀態的方法,就存在其對應的釋放方法,該方法為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;
    }
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);
    }

  獨占式釋放是非常簡單而且明確的。

  總結下獨占式同步狀態的獲取和釋放:在獲取同步狀態時,同步器維護一個同步隊列,獲取狀態失敗的線程都會被加入到隊列中併在隊列中進行自旋;移出隊列的條件是前驅節點為頭節點且成功獲取了同步狀態。在釋放同步狀態時,同步器調用tryRelease方法釋放同步狀態,然後喚醒頭節點的後繼節點。

  3.2 共用式同步狀態的獲取和釋放

  共用式同步狀態調用的方法是acquireShared,代碼如下:

public final void acquireShared(int arg) {
        //獲取同步狀態的返回值大於等於0時表示可以獲取同步狀態
        //小於0時表示可以獲取不到同步狀態  需要進入隊列等待
        if (tryAcquireShared(arg) < 0)
            doAcquireShared(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);
        }
    }
private void setHeadAndPropagate(Node node, int propagate) {
        Node h = head; // Record old head for check below
        //退出自旋的節點變成首節點
        setHead(node);
        /*
         * Try to signal next queued node if:
         *   Propagation was indicated by caller,
         *     or was recorded (as h.waitStatus either before
         *     or after setHead) by a previous operation
         *     (note: this uses sign-check of waitStatus because
         *      PROPAGATE status may transition to SIGNAL.)
         * and
         *   The next node is waiting in shared mode,
         *     or we don't know, because it appears null
         *
         * The conservatism in both of these checks may cause
         * unnecessary wake-ups, but only when there are multiple
         * racing acquires/releases, so most need signals now or soon
         * anyway.
         */
        if (propagate > 0 || h == null || h.waitStatus < 0 ||
            (h = head) == null || h.waitStatus < 0) {
            Node s = node.next;
            if (s == null || s.isShared())
                doReleaseShared();
        }
    }

  與獨占式一樣,共用式獲取也需要釋放同步狀態,通過調用releaseShared方法可以釋放同步狀態,代碼如下:

public final boolean releaseShared(int arg) {
        //釋放同步狀態
        if (tryReleaseShared(arg)) {
            //喚醒後續等待的節點
            doReleaseShared();
            return true;
        }
        return false;
    }
private void doReleaseShared() {
        /*
         * Ensure that a release propagates, even if there are other
         * in-progress acquires/releases.  This proceeds in the usual
         * way of trying to unparkSuccessor of head if it needs
         * signal. But if it does not, status is set to PROPAGATE to
         * ensure that upon release, propagation continues.
         * Additionally, we must loop in case a new node is added
         * while we are doing this. Also, unlike other uses of
         * unparkSuccessor, we need to know if CAS to reset status
         * fails, if so rechecking.
         */
        //自旋
    for (;;) {
            Node h = head;
            if (h != null && h != tail) {
                int ws = h.waitStatus;
                if (ws == Node.SIGNAL) {
                    if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
                        continue;            // loop to recheck cases
                    //喚醒後續節點
            unparkSuccessor(h);
                }
                else if (ws == 0 &&
                         !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
                    continue;                // loop on failed CAS
            }
            if (h == head)                   // loop if head changed
                break;
        }
    }

  unparkSuccessor方法和獨占式是一樣的。

  4、AQS應用

  AQS被大量的應用在了同步工具上。

  ReentrantLock:ReentrantLock類使用AQS同步狀態來保存鎖重覆持有的次數。當鎖被一個線程獲取時,ReentrantLock也會記錄下當前獲得鎖的線程標識,以便檢查是否是重覆獲取,以及當錯誤的線程試圖進行解鎖操作時檢測是否存在非法狀態異常。ReentrantLock也使用了AQS提供的ConditionObject,還向外暴露了其它監控和監測相關的方法。

  ReentrantReadWriteLock:ReentrantReadWriteLock類使用AQS同步狀態中的16位來保存寫鎖持有的次數,剩下的16位用來保存讀鎖的持有次數。WriteLock的構建方式同ReentrantLock。ReadLock則通過使用acquireShared方法來支持同時允許多個讀線程。

  Semaphore:Semaphore類(信號量)使用AQS同步狀態來保存信號量的當前計數。它裡面定義的acquireShared方法會減少計數,或當計數為非正值時阻塞線程;tryRelease方法會增加計數,在計數為正值時還要解除線程的阻塞。

  CountDownLatch:CountDownLatch類使用AQS同步狀態來表示計數。當該計數為0時,所有的acquire操作(對應到CountDownLatch中就是await方法)才能通過。

  FutureTask:FutureTask類使用AQS同步狀態來表示某個非同步計算任務的運行狀態(初始化、運行中、被取消和完成)。設置(FutureTask的set方法)或取消(FutureTask的cancel方法)一個FutureTask時會調用AQS的release操作,等待計算結果的線程的阻塞解除是通過AQS的acquire操作實現的。

  SynchronousQueues:SynchronousQueues類使用了內部的等待節點,這些節點可以用於協調生產者和消費者。同時,它使用AQS同步狀態來控制當某個消費者消費當前一項時,允許一個生產者繼續生產,反之亦然。

       除了這些j.u.c提供的工具,還可以基於AQS自定義符合自己需求的同步器。

 

       AQS就學習到這,如果有描述不當的地方,還請留言交流。瞭解了AQS後下一步準備詳細學習基於AQS的工具類。

 

參考資料:

https://github.com/lingjiango/ConcurrentProgramPractice

http://gee.cs.oswego.edu/dl/papers/aqs.pdf

《Java併發編程的藝術》


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 恢復內容開始 python爬蟲學習從0開始 第一次學習了python語法,迫不及待的來開始python的項目。首先接觸了爬蟲,是一個簡單爬蟲。個人感覺python非常簡潔,相比起java或其他面向對象的編程語言,動態語言不需要聲明函數或變數類型。python有20年的發展歷史,以簡潔高效聞名,pyt ...
  • 說概率前複習下歷史函數create_rand_list() #創建一個含有指定數量元素的listsum_fun() #累加len_fun() #統計個數multiply_fun() #累乘sum_mean_fun() #算數平均數sum_mean_rate() #算數平均數計算回報median_fu ...
  • 一、為什麼要用synchronized關鍵字 首先多線程中多個線程運行面臨共用數據同步的問題。 多線程正常使用共用數據時需要經過以下步驟: 1.線程A從共用數據區中複製出數據副本,然後處理。 2.線程A將處理好的數據副本寫入共用數據區。 3.線程B從共用數據區中複製出數據副本。 如此迴圈,直到線程結 ...
  • 最近維護一個項目,裡面用到ClientDataSet,由於之前接觸ClientDataSet比較少,所以這個星期補了一下關於ClientDataSet的知識,併在此記錄下我所瞭解到的並應用到實際項目中的ClientDataSet的知識。 項目新需求:1.從別的資料庫導入物料資料,並允許操作員做修改後 ...
  • 在最近的秋招中,阿裡和多益網路都問到了這個問題,雖然很簡單,但是我還是想總結一下,感興趣的可以看一下我的 "個人博客網站(Spring+MyBatis+redis+nginx+mysql)" (適合菜鳥),最近會抽空把最近面試遇到的問題總結一下。 本文針對問題:深克隆和淺克隆的區別和實現方式?(阿裡 ...
  • 1、數據結構 從不同的角度,可以分為邏輯結構和物理結構 邏輯結構:是數據元素之間的相互關係 集合結構 線性結構 樹形結構 圖形結構 物理結構:數據的邏輯結構在電腦的存儲形式 順序存儲結構:數據間的邏輯關係和物理關係一致 鏈式存儲結構 2、演算法時間複雜度 時間複雜度T(n)=O(f(n));f(n) ...
  • 由於需要在公司的內網進行神經網路建模試驗(https://www.cnblogs.com/NosenLiu/articles/9463886.html),為了更方便的在內網環境下快速的查閱資料,構建深度學習模型,我決定使用爬蟲來對深度學習框架keras的使用手冊進行爬取。 keras中文文檔的地址是 ...
  • 編程零基礎如何學習Python 如果你是零基礎,註意是零基礎,想入門編程的話,我推薦你學Python。雖然國內基本上是以C語言作為入門教學,但在麻省理工等國外大學都是以Python作為編程入門教學的。 那麼如何學習Python呢? 第一步:先把刀磨好 俗話說得好,磨刀不誤砍柴工,這個你不得不信,反正 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...