1.前言:
-
在Java中鎖所可以分爲兩大類,一類是通過synchrinized關鍵字實現的隱式鎖,一類是JUC包的鎖。前者是通過JVM實現的,後者是根據隊列同步器(AQS)實現的,也就是今天的主角。
-
在JUC包下實現了很多鎖以及工具類,例如ReentrantLock、ReadWriteLock、CountDownLatch、CyclicBarrier等,均是通過隊列同步器實現的,所以理解了隊列同步器的實現原理,對使用這些鎖及工具類或者閱讀這些類的源碼會有很大幫助。
2.什麼是AQS:
-
隊列同步器的全稱是
AbstractQueuedSynchronizer
,簡稱AQS,翻譯過來就是抽象的隊列同步器。從命名就能猜出,這個類是一個抽象類,且是基於隊列來實現的一個同步器。JUC包下所有的鎖都是基於它來實現的。在AQS中定義了一個int類型的變量:state
,用它來表示同步狀態,哪個線程成功對state變量進行了加1操作,那麼這個線程就持有了鎖;AQS中還定義了一個FIFO(先進先出)的隊列
,用來表示等待獲取鎖的線程。 -
在計算機領域,鎖的實現都可以用管程模型來實現。管程模型的示意圖如下,在管程模型中存在兩個概念:入口等待隊列和條件等待隊列。既然鎖都可以來管程來實現,那麼JUC包下實現的鎖中是不是也存在這
入口等待隊列
和條件等待隊列
呢?答案是肯定的。AQS中也存着兩個隊列:同步隊列
和條件等待隊列
,它們分別對應管程中的入口等待隊列
和條件等待隊列
。今天先分析AQS中的同步隊列
的數據結構和實現原理,關於AQS中條件等待隊列
會在Condition
類的源碼分析中講解。
3.AQS中的方法:
-
AQS類提供了很多方法,既然是一個抽象類,就會有方法需要子類去重寫。AQS中有如下方法需要子類重寫。
方法 | 作用 |
---|---|
protected boolean tryAcquire(int arg) | 獨佔式嘗試獲取鎖 |
protected boolean tryRelease(int arg) | 獨佔式嘗試釋放鎖 |
protected int tryAcquireShared(int arg) | 共享式嘗試獲取鎖 |
protected boolean tryReleaseShared(int arg) | 共享式嘗試釋放鎖 |
protected boolean isHeldExclusively() | 當前線程是否獨佔式的佔用鎖 |
上面子類重寫的方法中,獲取或者釋放鎖時都會嘗試去修改同步狀態state的值,在AQS中提供了三個和同步狀態相關的方法。(上面的方法說明中都是用嘗試
二字,這是因爲調用這些方法不一定能獲取鎖成功或者釋放鎖成功)
方法 | 作用 |
---|---|
int getState() | 獲取同步狀態state的值 |
void setState(int newState) | 修改同步狀態。通常是隻有已經獲取到鎖的線程才調用這個方法去修改同步狀態,這個時候因爲只有一個線程能取到鎖,所以不用擔心併發問題 |
boolean compareAndSetState(int expect, int update) | 通過CAS的方式去修改同步狀態,當多個線程同時嘗試修改state時使用,它能保證只有一個線程能修改成功 |
-
AQS的設計採用了模板設計模式,它定義了很多模板方法,在模板方法中會調用由子類重寫的方法。這樣就抽象出了鎖實現的通用邏輯,而針對不同類型的鎖的實現,只需要有給不同類型鎖的同步組件在重寫的方法中實現自己特有的邏輯即可。下面列出部分模板方法及其作用。
方法 | 作用 |
---|---|
int getState() | 獲取同步狀態state的值 |
void setState(int newState) | 修改同步狀態。通常是隻有已經獲取到鎖的線程才調用這個方法去修改同步狀態,這個時候因爲只有一個線程能取到鎖,所以不用擔心併發問題 |
boolean compareAndSetState(int expect, int update) | 通過CAS的方式去修改同步狀態,當多個線程同時嘗試修改state時使用,它能保證只有一個線程能修改成功 |
-
AQS的設計採用了模板設計模式,它定義了很多模板方法,在模板方法中會調用由子類重寫的方法。這樣就抽象出了鎖實現的通用邏輯,而針對不同類型的鎖的實現,只需要有給不同類型鎖的同步組件在重寫的方法中實現自己特有的邏輯即可。下面列出部分模板方法及其作用。
方法 | 作用 |
---|---|
void acquire(int arg) | 獨佔式獲取同步狀態,如果線程成功獲取了同步狀態,則方法會返回,如果沒有獲取到同步狀態,那麼當前線程就會進入到同步隊列中,並阻塞。該方法對中斷無法響應 |
void acquireInterruptibly(int arg) throws InterruptedException | 和acquire() 方法一樣,不過該方法能響應中斷 |
boolean tryAcquireNanos(int arg,long nanosTimeout) throws InterruptedException | 在acquireInterruptibly() 方法的基礎上增加了超時限制,當指定時間內如果沒有獲取到同步鎖,就會返回false。 |
void acquireShared(int arg) | 共享式獲取同步狀態,如果線程成功獲取到了同步狀態,那麼方法就會返回。否則進入到同步隊列中進行等待,並阻塞。它與acquire() 的區別是,該方法能允許多個線程同時獲取到鎖 |
void acquireSharedInterruptibly(int arg) throws InterruptedException | 在acquireShared() 方法的基礎上增加了響應中斷的功能 |
boolean tryAcquireSharedNanos(int arg, long nanosTimeout) throws InterruptedException | 在acquireSharedInterruptibly() 基礎上增加了超時功能,在指定時間內如果沒有獲取到鎖,就會返回false |
boolean release(int arg) | 獨佔式釋放鎖 |
boolean releaseShared(int arg) | 共享式釋放鎖 |
-
從上面的方法中可以發現,這幾個模板都是成對出現的,獨佔式和共享式獲取鎖,能響應中斷的獨佔式和共享式獲取鎖,能超時的獨佔式和共享式獲取鎖,釋放獨佔式和共享式鎖。所以實際上我們只需要弄明白
acquire()
方法和release()
方法即可,其他的方法與這兩個方法的實現幾乎一樣,只是改變了部分邏輯。 -
獨佔式和共享式的區別:獨佔式表示的是隻能有一個線程獲取到鎖,而共享式表示的是同一時刻允許有多個線程獲取到鎖。前者的實際應用有
ReentrantLock
,後者的實際應用有ReentrantReadWriteLock、CountDownLatch、CyclicBarrier
等。
4. 同步隊列的設計原理:
AQS中兩大核心:同步狀態
和同步隊列
。同步狀態由state這個int類型的全局變量實現,哪個線程成功修改了state的值,就表示這個線程獲取到了鎖或者釋放了鎖。同步隊列是一個遵循先進先出(FIFO)
的隊列,它是一個由Node節點組成的雙向鏈表。每一個線程在獲取同步狀態時,如果獲取同步狀態失敗,就會將當前線程封裝成一個Node,然後將其加入到同步隊列中。Node是AQS裏面的一個靜態內部類,Node這個數據結構中,包含了5個屬性。
屬性名 |
作用 |
---|---|
Node prev | 同步隊列中,當前節點的前一個節點,如果當前節點是同步隊列的頭結點,那麼prev屬性爲null |
Node next | 同步隊列中,當前節點的後一個節點,如果當前節點是同步隊列的尾結點,那麼next屬性爲null |
Node thread | 當前節點代表的線程,如果當前線程獲取到了鎖,那麼當前線程所代表的節點一定處於同步隊列的隊首,且thread屬性爲null,至於爲什麼要將其設置爲null,這是AQS特意設計的。 |
int waitStatus | 當前線程的等待狀態,有5種取值。0表示初始值,1表示線程被取消,-1表示當前線程處於等待狀態,-2表示節點處於等待隊列中,-3表示下一次共享式同步狀態獲取將會無條件地被傳播下去 |
Node nextWaiter | 等待隊列中,該節點的下一個節點 |
通過Node的prev
屬性和next
屬性就構成了一個雙向鏈表,也就是AQS中的同步隊列,但是想要通過這個隊列找到隊列中的每一個元素,我們就需要知道這個隊列的頭結點是誰,尾結點是誰。因此AQS中又提供了兩個屬性:head
和tail
,這兩個屬性的類型均是Node類型,它們分別指向同步隊列中的頭結點和尾結點。這樣AQS就能通過head和tail,找到隊列中的每一個元素。同步隊列的結構示意圖如下。
4.2 實現原理:
當一個線程調用acquire()
方法獲取同步狀態的時候,如果此時能成功獲取到同步狀態,那麼就直接返回;如果不能獲取到同步狀態,此時就表示同步狀態已經被其他線程獲取到了,那麼這個時候,當前線程就需要開始等待,那麼如何實現等待呢?此時當前線程會先將自己封裝成一個Node
,然後這個Node加入到同步隊列中
。在加入到同步隊列之前,需要判斷隊列有沒有被初始化
,即隊列中有沒有節點存在。如果head=null
則表示當前同步隊列還沒有初始化
,所以這個時候當前線程做的第一件事,就是初始化隊列。如何初始化呢?當前線程需要先初始化head節點,因此它會new一個Node,然後將這個Node賦值給head,注意head節點表示的是獲取到同步狀態的線程。接着當前線程再將自己封裝成一個Node,然後將head的next屬性指向這個Node
,這樣就將自己加入到了隊列中。注意head節點的thread屬性始終都是null
,因爲head節點是當前線程創建的,而當前線程只知道有線程獲取到了同步狀態,但是卻不知道是誰獲取到了,所以此時當前線程在初始化head節點的時候,只能讓head節點的thread屬性爲null。當前線程再將自己加入到隊列之後,還需要將tail指向自己。在設置head屬性和tail屬性
時,由於存在多個線程併發的可能
,所以需要使用AQS提供的compareAndSetHead()、compareAndSetTail()
方法,這兩個方法會調用Unsafe類的CAS方法,能保證原子性。節點加入到同步隊列的示意圖如下。
同步隊列中首節點表示的是獲取到同步狀態的線程,當首節點代表的線程釋放了同步狀態,由於AQS遵循FIFO
,所以此時線程在釋放同步狀態後還需要喚醒後面節點的線程去獲取同步狀態。當有線程獲取到同步狀態後,需要將自己代表的節點設置爲同步隊列的首節點。由於此時肯定只有一個線程獲取到同步狀態
,因此此時在更新head屬性時,不需要通過CAS方法來保證原子性
,只需要使用setHead()
方法即可。首節點的設置的示意圖如下。
如果感覺有收穫,麻煩點一個贊,您的鼓勵,是筆者創作中最大的動力,謝謝!