您好,登錄后才能下訂單哦!
這篇文章主要介紹“Java中AQS的原理及作用是什么”,在日常操作中,相信很多人在Java中AQS的原理及作用是什么問題上存在疑惑,小編查閱了各式資料,整理出簡單好用的操作方法,希望對大家解答”Java中AQS的原理及作用是什么”的疑惑有所幫助!接下來,請跟著小編一起來學習吧!
java中AQS是AbstractQueuedSynchronizer類,AQS依賴FIFO隊列來提供一個框架,這個框架用于實現鎖以及鎖相關的同步器,比如信號量、事件等。
在AQS中,主要有兩部分功能,一部分是操作state變量,第二部分是實現排隊和阻塞機制。
注意,AQS并沒有實現任何同步接口,它只是提供了類似acquireInterruptible的方法,調用這些方法可以實現鎖和同步器。
java使用MESA管程模型來管理類的成員變量和方法,讓這個類的成員變量和方法的操作是線程安全的。下圖是MESA管程模型,里面除了定義共享變量外,還定義了條件變量和條件變量等待隊列:
java中的MESA模型有一點改進,就是管程內部只有一個條件變量和一個等待隊列。下圖是AQS的管程模型:
AQS的管程模型依賴AQS中的FIFO隊列實現入口等待隊列,而ConditionObject則實現了條件隊列,這個隊列可以創建多個。本文主要講解入口等待隊列獲取鎖的幾種方式。參考1[1]
獨占, 忽略interrupts
public final void acquire(int arg) { if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) selfInterrupt(); }
這里的tryAcquire是抽象方法,有AQS的子類來實現,因為每個子類實現的鎖是不一樣的。
入隊
上面的代碼可以看到,獲取鎖失敗后,會先執行addWaiter方法加入隊列,然后執行acquireQueued方法自旋地獲取鎖直到成功。
addWaiter代碼邏輯如下圖,簡單說就是把node入隊,入隊后返回node參數給acquireQueued方法:
這里有一個點需要注意,如果隊列為空,則新建一個Node作為隊頭。
入隊后獲取鎖
acquireQueued自旋獲取鎖邏輯如下圖:
這里有幾個細節:
1.waitStatus
CANCELLED(1):當前節點取消獲取鎖。當等待超時或被中斷(響應中斷),會觸發變更為此狀態,進入該狀態后節點狀態不再變化。
SIGNAL(-1):后面節點等待當前節點喚醒。
CONDITION(-2):Condition中使用,當前線程阻塞在Condition,如果其他線程調用了Condition的signal方法,這個結點將從等待隊列轉移到同步隊列隊尾,等待獲取同步鎖。
PROPAGATE(-3):共享模式,前置節點喚醒后面節點后,喚醒操作無條件傳播下去。
0:中間狀態,當前節點后面的節點已經喚醒,但是當前節點線程還沒有執行完成。
2.獲取鎖失敗后掛起
如果前置節點不是頭節點,或者前置節點是頭節點但當前節點獲取鎖失敗,這時當前節點需要掛起,分三種情況,
前置節點waitStatus=-1,如下圖:
前置節點waitStatus > 0,如下圖:
前置節點waitStatus < 0 但不等于 -1,如下圖:
3.取消獲取鎖
如果獲取鎖拋出異常,則取消獲取鎖,如果當前節點是tail節點,分兩種情況如下圖:
如果當前節點不是tail節點,也分兩種情況,如下圖:
4.對中斷狀態忽略
5.如果前置節點的狀態是 0 或 PROPAGATE,會被當前節點自旋過程中更新成-1,以便之后通知當前節點。
獨占 + 響應中斷
對應方法acquireInterruptibly(int arg)。
跟忽略中斷(acquire方法)不同的是要響應中斷,下面兩個地方響應中斷:
獲取鎖之前會檢查當前線程是否中斷。
獲取鎖失敗入隊,在隊列中自旋獲取鎖的過程中也會檢查當前線程是否中斷。
如果檢查到當前線程已經中斷,則拋出InterruptedException,當前線程退出。
獨占 + 響應中斷 + 考慮超時
對應方法tryAcquireNanos(int arg, long nanosTimeout)。
這個方法具備了獨占 + 響應中斷 + 超時的功能,下面2個地方要判斷是否超時:
自旋獲取鎖的過程中每次獲取鎖失敗都要判斷是否超時
獲取鎖失敗park之前要判斷超時時間是否大于自旋的閾值時間**(spinForTimeoutThreshold = 1ns)**
另外,park線程的操作使用parkNanos傳入阻塞時間。
獨占鎖釋放分兩步:釋放鎖,喚醒后繼節點。
釋放鎖的方法 tryRelease 是抽象的,由子類去實現。
我們看一下喚醒后繼節點的邏輯,首先需要滿足兩個條件:
head節點不等于 null
head節點waitStatus不等于0
這里有兩種情況(在方法unparkSuccessor):
情況一,后繼節點waitStatus <= 0,直接喚醒后繼節點,如下圖:
情況二:后繼節點為空或者waitStatus > 0,從后往前查找最接近當前節點的節點進行喚醒,如下圖:
之前我們講了獨占鎖,這一小節我們談共享鎖,有什么不同呢?
共享,忽略interrupts
對應方法acquireShared,代碼如下:
public final void acquireShared(int arg) { if (tryAcquireShared(arg) < 0) doAcquireShared(arg); }
tryAcquireShared
這里獲取鎖使用的方法是tryAcquireShared,獲取的是共享鎖。獲取共享鎖跟獲取獨占鎖不同的是,會返回一個整數值,說明如下:
返回負數:獲取鎖失敗。
返回0:獲取鎖成功但是之后再由線程來獲取共享鎖時就會失敗。
返回正數:獲取鎖成功而且之后再有線程來獲取共享鎖時也可能會成功。所以需要把喚醒操作傳播下去。
tryAcquireShared獲取鎖失敗后(返回負數),就需要入隊后自旋獲取,也就是執行方法doAcquireShared。
doAcquireShared
怎么判斷隊列中等待節點是在等待共享鎖呢?nextWaiter == SHARED,這個參數值是入隊新建節點的時候構造函數傳入的。
自旋過程中,如果獲取鎖成功(返回正數),首先把自己設置成新的head節點,然后把通知傳播下去。如下圖:
之后會喚醒后面節點并保證喚醒操作可以傳播下去。但是需要滿足四個條件中的一個:
tryAcquireShared返回值大于0,有多余的鎖,可以繼續喚醒后繼節點
舊的head節點waitStatus < 0,應該是其他線程釋放共享鎖過程中把它的狀態更新成了-3
新的hade節點waitStatus < 0,只要不是tail節點,就可能是-1
這里會造成不必要的喚醒,因為喚醒后獲取不到鎖只能繼續入隊等待
當前節點的后繼節點是空或者非空但正在等待共享鎖
喚醒后面節點的操作,其實就是釋放共享鎖,對應方法是doReleaseShared,見釋放共享鎖一節。
共享 + 響應中斷
對應方法acquireSharedInterruptibly(int arg)。
跟共享忽略中斷(acquireShared方法)不同的是要響應中斷,下面兩個地方響應中斷:
獲取鎖之前會檢查當前線程是否中斷。
獲取鎖失敗入隊,在隊列中自旋獲取鎖的過程中也會檢查當前線程是否中斷。
如果檢查到當前線程已經中斷,則拋出InterruptedException,當前線程退出。
共享 + 響應中斷 + 考慮超時
對應方法tryAcquireSharedNanos(int arg, long nanosTimeout)。
這個方法具備了共享 + 響應中斷 + 超時的功能,下面2個地方要判斷是否超時:
自旋獲取鎖的過程中每次獲取鎖失敗都要判斷是否超時
獲取鎖失敗park之前要判斷超時時間是否大于自旋的閾值時間(spinForTimeoutThreshold = 1ns)
另外,park線程的操作使用parkNanos傳入阻塞時間。
釋放共享鎖代碼如下:
public final boolean releaseShared(int arg) { if (tryReleaseShared(arg)) { doReleaseShared(); return true; } return false; }
首先嘗試釋放共享鎖,tryReleaseShared代碼由子類來實現。釋放成功后執行AQS中的doReleaseShared方法,是一個自旋操作。
自旋的條件是隊列中至少有兩個節點,這里分三種情況。
情況一:當前節點waitStatus是-1,如下圖:
情況二:當前節點waitStatus是0(被其他線程更xin新成了中間狀態),如下圖:
情況三:當前節點waitStatus是-3,為什么會這樣呢?需要解釋一下,head節點喚醒后繼節點之前waitStatus已經被更新中間態0了,喚醒后繼節點動作還沒有執行,又被其他線程更成了-3,也就是其他線程釋放鎖執行了上面情況二。這時需要先把waitStatus再更成0(在方法unparkSuccessor),如下圖:
上面的講解可以看出,如果要基于AQS來實現并發鎖,可以根據需求重寫下面四個方法來實現,這四個方法在AQS中沒有具體實現:
tryAcquire(int arg):獲取獨占鎖
tryRelease(int arg):釋放獨占鎖
tryAcquireShared(int arg):獲取共享鎖
tryReleaseShared(int arg):釋放共享鎖
參考2[2]
AQS的子類需要重寫上面的方法來修改state值,并且定義獲取鎖或者釋放鎖時state值的變化。子類也可以定義自己的state變量,但是只有更新AQS中的state變量才會對同步起作用。
還有一個判斷當前線程是否持有獨占鎖的方法 isHeldExclusively,也可以供子類重寫后使用。
獲取/釋放鎖的具體實現放到下篇文章講解。
AQS使用FIFO隊列實現了一個鎖相關的并發器模板,可以基于這個模板來實現各種鎖,包括獨占鎖、共享鎖、信號量等。
AQS中,有一個核心狀態是waitStatus,這個代表節點的狀態,決定了當前節點的后續操作,比如是否等待喚醒,是否要喚醒后繼節點。
到此,關于“Java中AQS的原理及作用是什么”的學習就結束了,希望能夠解決大家的疑惑。理論與實踐的搭配能更好的幫助大家學習,快去試試吧!若想繼續學習更多相關知識,請繼續關注億速云網站,小編會繼續努力為大家帶來更多實用的文章!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。