Java之AQS代碼原理解析

        AQS(AbstractQueuedSynchronizer)是各種鎖實現的基礎,提供了對資源(state字段)的獲取與阻塞等待,阻塞的線程會被放進一個先進先出(FIFO)的同步隊列裏。各種鎖是AQS的子類,子類必須實現一套用來改變state變量(volatile 修飾的變量)的方法,包括鎖資源的獲取方法與鎖資源的釋放方法。始終記得:volitile和cas操作鑄就了AQS的輝煌

 

       衆所周知,鎖分排他鎖和共享鎖, AQS對鎖的獲取與釋放也是分兩種情況的,即SHARED與EXCLUSIVE兩種模式。即如下圖代碼:

        EXCLUSIVE模式的鎖必須實現tryAcquire和tryRelease兩個抽象方法,同理,SHARED模式的鎖必須實現tryAcquireShared和tryReleaseShared兩個抽象方法。至於爲什麼AQS是個抽象類而不是接口的原因就在於次,比如你只需要排他鎖就只用實現你需要的那兩個方法,而不需要像接口那樣需要實現全部抽象方法。當然也有同時實現兩套方法的鎖,如ReadWriteLock.


state資源

        state字段是AQS鎖的核心,即是鎖資源,該字段是volatile修飾的。volatile主要對所修飾的變量提供兩個功能:①可見性②防止指令重排序。AQS框架都是對state變量的CAS增減操作,不通的增減方式從而實現了不同性質的鎖,例如重入鎖在同對象再次重入該鎖鎖住的資源時候,會對state字段進行加一操作。操作state字段有三個方法:

getState()

setState()

compareAndSetState()

這三個均是原子操作,compareAndSetState的實現依賴於Unsafe的compareAndSwapInt()方法,即

自定義鎖方法

        如剛纔介紹,有兩種鎖,有兩套方法。每次自定義鎖實現的時候需要自己去實現自己需要的方法。默認情況下不重寫的方法返回拋出UnsupportedOperationException異常。以下是需要重寫的方法:

isHeldExclusively():該線程是否正在獨佔資源。只有用到condition才需要去實現它。

tryAcquire(int):獨佔方式。嘗試獲取資源,成功則返回true,失敗則返回false。

tryRelease(int):獨佔方式。嘗試釋放資源,成功則返回true,失敗則返回false。

tryAcquireShared(int):共享方式。嘗試獲取資源。負數表示失敗;0表示成功,但沒有剩餘可用資源;正數表示成功,且有剩餘資源。

tryReleaseShared(int):共享方式。嘗試釋放資源,如果釋放後允許喚醒後續等待結點返回true,否則返回false。

源碼及原理

一、排他鎖

1、acquire(int arg)

該方法調用後,首先回去嘗試調用tryAcquire獲取資源,如果獲取到資源則直接返回。獲取不到則調用addWaiter方法向隊列尾部插入一個新節點,再調用acquireQueued方法使節點進入一個安全點休息,等待後續喚起

1.1、addWaiter(Node mode)

該方法將剛剛構造好的node節點用cas操作設置成尾結點,並修改指針成功加入隊尾,如果隊列爲空則使用enq方法新構造一個隊列放入。

 

1.1.1、enq(final Node node)

        使用for無限循環,取到tail節點指針指的尾節點t,如果t爲空則使用cas操作head指向一個新構造的沒有任何任務線程的空節點當做頭指針,如果cas設置成功則將tail節點也指向該空節點。如果t不爲空,意味着併發情況下他人已經構建了一個隊列,那麼將node的prev指針指向節點t,並通過cas將隊尾設置成要加入的節點node,cas成功後再將之前的尾結點指向入隊的新節點,到此入隊成功。

重要須知:head和tail兩個字段均爲volitile修飾的,意味着cas操作的時候會使字段值失效並重新從主存讀取,也就保證了併發情況下,不會構建多個隊列,也不會同時有多個節點入隊指向同一個前驅節點的問題。

 

1.2、acquireQueued(final Node node, int arg)

        首先提一個非常重要的點,該方法代碼中interrupted字段的作用是用來補標記位的,當方法內部最後調用parkAndCheckInterrupt後因爲Thread.interrupted()會清除當前線程的中斷標記位。所以要在方法最後將中斷標記位補上。parkAndCheckInterrupt方法代碼後面講。

        該方法意思是在一個無窮for循環中,每一次執行都去拿到該節點的前驅節點p,當前驅節點p是頭結點且能獲得到資源,那麼就認爲這個節點可以執行了,就將頭結點指針指向當前節點,並將前驅節點的next指針指向空,使得gc可達性算法能將其識別爲垃圾回收,並直接返回成功。但是當前驅節點不是頭結點或者嘗試獲取資源失敗後,就要執行shouldParkAfterFailedAcquire來判斷是否需要將該節點線程掛起。因爲是短路與(&&),所以前者如果前者未達到掛起的要求,則返回false,將不執行parkAndCheckInterrupt方法。而當滿足需要掛起的條件時則返回true,並調用parkAndCheckInterrupt來掛起線程。在掛起後如果被打斷parkAndCheckInterrupt方法則會返回true,並通過剛纔所說的補標記,將標記位補上。

acquireQueued的整體流程:

1、結點進入隊尾

2、檢查狀態,找到安全休息點;

3、調用park()進入waiting狀態,等待unpark()或interrupt()喚醒自己;

4、喚醒後,看自己是不是有資格能拿到資源。如果拿到,head指向當前結點,並返回從入隊到拿到號的整個過程中是否被中斷過;如果沒拿到,繼續流程2。

 

1.2.1、shouldParkAfterFailedAcquire(Node pred, Node node)

        該方法是用來判斷是否能夠將當前節點掛起的方法。首先我們要清楚Node的各種status含義:

status初始化都是int默認值0,被取消:1,後繼節點可以被掛起(自身被掛起,等待喚醒):-1,condition等待:-2,可傳遞狀態:-3.

因爲我們目前看的是排他鎖,所以忽略PROPAGATE狀態。

通過代碼我們可以知道,這個方法設置前驅節點的狀態值並返回true或false。

①當前驅節點狀態是SIGNAL,意味着前驅節點已經掛起,這時候自身節點則到達可以掛起的安全點,返回true

②當前驅pred節點的狀態是被取消了的,那麼將不斷向前找節點,直到找到一個前面的pred節點的狀態<=0,並設置當前node節點的prev指針指向該節點,意味着忽略被取消的節點。返回false

③如果狀態是0或者-3,那將cas設置pred節點的狀態爲SIGNAL。返回false

 

單獨看這個方法感覺並沒有很厲害,但是不要忘了這個方法是嵌套在外層方法的for循環裏的,綜合外層的方法可以明白:不停的去檢驗是否能獲取到資源及如果獲取不到資源再通過該方法尋找到一個安全點,找到一個安全點以後才能掛起

 

到此我們需要梳理一下一個新任務來搶奪資源後的過程:

 

到此我們可以大體清晰的看到入隊及掛起的流程,這只是最簡單的樣子,現實情況會比這個複雜很多

1.2.2、parkAndCheckInterrupt()

調用LockSupport.park(Object blocker) 方法,線程掛起,注意掛起後不會return,只有在①被unpark或②被interrupt後纔可以執行。喚醒後繼節點的方法只有這兩種,最後需要注意的是Thread.interrupted()會清除當前線程的中斷標記位(重複一下,很重要,剛纔已經提過)

1.2.3、cancelAcquire(Node node)

        該方法在acquireQueued方法發生在acquireQueued方法的for循環排隊獲取資源之後的finally裏。當獲取資源失敗且發生了異常時,用來取消仍在進行嘗試獲取資源的該節點。

 

        可以從代碼裏看到先找到一個狀態不是被取消的前面的節點(pred),並將自身的prev指針指向這個pred節點。

        之後判斷是不是尾結點,如果是尾結點,則用cas將tail指針指向pred,並將pred的next指針指向null,來幫助觸發gc。

        如果是頭結點,則直接調用unparkSuccessor來喚醒後繼節點,將傳遞性延續下去。如果既不是頭也不是尾結點的時候,①pred的狀態不爲SIGNAL,且再次判斷狀態不是CANCELLED,且cas設置狀態爲SIGNAL失敗,則也直接喚醒後繼節點 。②當pred節點的thread爲空,也就是剛enq隊列時使用的空任務節點,則也直接喚醒後繼節點。③前驅節點爲SIGNAL或者cas設置爲SIGNAL成功,且前驅節點的thread任務不爲null,則用cas設置pred節點的next指針指向要取消節點的後繼節點。並且將要取消節點的next指針指向自己,用來觸發gc回收自己。

        疑點:有人可能會問,問啥用node.next = node;就可以觸發gc了,剛纔遍歷的node節點前狀態爲被取消的那些節點的prev和next指針都還沒改啊?而且要取消的節點的prev指針也還在啊?以下是個人理解:

        對於第一個問題,那些遍歷過爲被取消狀態的節點他的next也是指向自身的。而他們的prev指針也會像下一個問題的處理方式一樣被處理。

        對於第二個問題,被取消狀態的節點肯定也是執行過cancelAcquire方法的,所以prev也是指向一個當時SIGNAL狀態的節點,當前驅執行完用tryAcquire將指向執行完的節點的prev置爲null,就完成了prev指針的回收(這塊屬於個人理解,如果有異議,請盡情糾正我,讓我學習一下)

 

1.3、selfInterrupt()

該方法很簡單,只是調用了Thread的方法,當acquireQueued返回true的時候以爲這被打斷過,就是上述提到的要補標記。

 

2、release(int arg)

        嘗試調用tryRelease釋放資源,成功喚醒後繼節點。喚醒前要判斷隊列頭節點是不是null且是否不爲狀態0,都滿足則調用unparkSuccessor嘗試喚醒隊列裏的下一個結點

2.1、unparkSuccessor(Node node)

        獲取要當前node節點狀態,如果狀態小於0,則用cas設置狀態爲0,以表示執行成功。

        獲取node的後繼節點s。如果s不爲null則用LockSupport.unpark(s.thread);喚醒s節點任務。如果s爲null或者s節點的狀態爲被取消,則意味着該節點無效,需要找一個喚醒,那麼Doug Lea是怎麼做的呢?我們看到一段非常牛逼的代碼,從隊尾往前倒推,找到一個狀態小於等於0的節點來喚醒。

         疑點:爲什麼是從隊尾往前倒推找節點喚醒呢?

        從tail開始倒推查找,原因在於enq方法插入是,新節點prev指向tail,tail再指向新節點。這裏後繼節點指向前驅的指針是由cas操作保證線程安全的。而cas操作之後t.next=node之前,可能會有其他線程進來。所以出現了問題。所以從尾部倒推查找是一定能遍歷到所有節點的。

 

排他鎖獲取流程總結:

1、調用自定義同步器的tryAcquire()嘗試直接獲取資源,如果成功則直接返回;

2、沒成功,則addWaiter()將該線程加入等待隊列的尾部,並標記爲獨佔模式;

3、acquireQueued()使線程在等待隊列中休息,有機會時(輪到自己,會被unpark())會去嘗試獲取資源。獲取到資源後才返回。如果在整個等待過程中被中斷過,則返回true,否則返回false。

4、如果線程在等待過程中被中斷過,它是不響應的。只是獲取資源後纔再進行自我中斷selfInterrupt(),將中斷補上。

二、共享鎖

1、acquireShared(int arg)

        共享鎖的獲取在於資源可以不支持多人共享獲取。在嘗試獲取資源,tryAcquireShared返回結果小於0表示獲取失敗,會調用doAcquireShared嘗試重新獲取,進入隊列並掛起等待後續喚醒。如果獲取到資源且資源還沒有使用完的情況下,可以將資源延續下去,去喚醒後繼節點。例如資源數爲10,一號任務需要5資源,二號任務需要4資源,三號任務需要1資源,那麼一號獲取到資源執行後還會嘗試喚醒二號,二號喚醒獲取到資源後還會嘗試喚醒3號。但是不能跳過順序使後邊節點越級獲取。例如資源數爲10,一號任務需要6資源,二號任務需要5資源,三號任務需要4資源,那麼一號獲取到資源執行後還會嘗試喚醒二號,二號在判斷時候發現自己不滿足資源,那麼他將掛起自己並不會有喚醒後繼節點的操作。

1.1、doAcquireShared(int arg)

        將當前線程加入等待隊列尾部休息,直到其他線程釋放資源喚醒自己,自己成功拿到相應量的資源後才返回。這裏會發現獲取到資源後調用setHeadAndPropagate設置頭結點並在還有資源的情況下調用doReleaseShared只喚醒下一個節點。下一個節點如果沒被喚醒就等着,這樣不會造成越級喚醒導致順序混亂。源碼如下:

2、releaseShared(int arg)

        該方法是共享模式下線程釋放共享資源的頂層入口。它會釋放指定量的資源,如果成功釋放且允許喚醒等待線程,它會喚醒等待隊列裏的其他線程來獲取資源。

        此方法的流程也比較簡單,釋放掉資源後,喚醒後繼。跟獨佔模式下的release()相似,但有一點稍微需要注意:獨佔模式下的tryRelease()在完全釋放掉資源(state=0)後,纔會返回true去喚醒其他線程,這主要是基於獨佔下可重入的考量;而共享模式下的releaseShared()則沒有這種要求,共享模式實質就是控制一定量的線程併發執行,那麼擁有資源的線程在釋放掉部分資源時就可以喚醒後繼等待結點。

2.1、doReleaseShared()

        for循環,大體同獨佔鎖邏輯,如果隊列頭節點不爲空,且head不等於tail,即還有後繼節點,則繼續執行難,否則直接沒有任務直接返回。如果頭結點的狀態爲SIGNAL,cas將head節點狀態設置爲0,以表示執行完成。之後調用unparkSuccessor喚醒後繼節點。如果不爲SIGNAL,但爲0且能夠cas設置爲PROPAGATE並進行下一次循環。如果頭結點沒有改變則跳出了循環。PROPAGATE狀態是在共享模式下頭結點有可能處於的狀態,表示鎖的下一次獲取可以無條件傳播

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