掃描下方二維碼或者微信搜索公衆號
菜鳥飛呀飛
,便可關注微信公衆號,閱讀更多Spring源碼分析
和Java併發編程
文章。編程
AbstractQueuedSynchronizer
,簡稱AQS,翻譯過來就是抽象的隊列同步器。從命名就能猜出,這個類是一個抽象類,且是基於隊列來實現的一個同步器。JUC包下全部的鎖都是基於它來實現的。在AQS中定義了一個int類型的變量:state
,用它來表示同步狀態,哪一個線程成功對state變量進行了加1操做,那麼這個線程就持有了鎖;AQS中還定義了一個FIFO(先進先出)的隊列
,用來表示等待獲取鎖的線程。入口等待隊列
和條件等待隊列
呢?答案是確定的。AQS中也存着兩個隊列:同步隊列
和條件等待隊列
,它們分別對應管程中的入口等待隊列
和條件等待隊列
。今天先分析AQS中的同步隊列
的數據結構和實現原理,關於AQS中條件等待隊列
會在Condition
類的源碼分析中講解。《Java併發編程實戰》
一課中的第一部分第8講:管程:併發編程的萬能鑰匙
【圖】方法 | 做用 |
---|---|
protected boolean tryAcquire(int arg) | 獨佔式嘗試獲取鎖 |
protected boolean tryRelease(int arg) | 獨佔式嘗試釋放鎖 |
protected int tryAcquireShared(int arg) | 共享式嘗試獲取鎖 |
protected boolean tryReleaseShared(int arg) | 共享式嘗試釋放鎖 |
protected boolean isHeldExclusively() | 當前線程是否獨佔式的佔用鎖 |
嘗試
二字,這是由於調用這些方法不必定能獲取鎖成功或者釋放鎖成功)方法 | 做用 |
---|---|
int getState() | 獲取同步狀態state的值 |
void setState(int newState) | 修改同步狀態。一般是隻有已經獲取到鎖的線程才調用這個方法去修改同步狀態,這個時候由於只有一個線程能取到鎖,因此不用擔憂併發問題 |
boolean compareAndSetState(int expect, int update) | 經過CAS的方式去修改同步狀態,當多個線程同時嘗試修改state時使用,它能保證只有一個線程能修改爲功 |
方法 | 做用 |
---|---|
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
等。這些類的源碼以及實現原理後面會有文章專門分析。要想讀懂AQS的源代碼,首先須要明白它的設計原理,不然很難看明白其中的邏輯。畢竟代碼只是具體實現的工具,編程語言能夠多變,但設計原理是不變的。設計模式
同步狀態
和同步隊列
。同步狀態由state這個int類型的全局變量實現,哪一個線程成功修改了state的值,就表示這個線程獲取到了鎖或者釋放了鎖。同步隊列是一個遵循先進先出(FIFO)
的隊列,它是一個由Node節點組成的雙向鏈表。每個線程在獲取同步狀態時,若是獲取同步狀態失敗,就會將當前線程封裝成一個Node,而後將其加入到同步隊列中。Node是AQS裏面的一個靜態內部類,Node這個數據結構中,包含了5個屬性,每一個屬性的功能以下列表。Node就是經過這5個屬性來實現同步隊列
和等待隊列
的,關於等待隊列今天先暫時不分析,後面在分析Condition
源碼時會詳細分析。屬性名 | 做用 |
---|---|
Node prev | 同步隊列中,當前節點的前一個節點,若是當前節點是同步隊列的頭結點,那麼prev屬性爲null |
Node next | 同步隊列中,當前節點的後一個節點,若是當前節點是同步隊列的尾結點,那麼next屬性爲null |
Node thread | 當前節點表明的線程,若是當前線程獲取到了鎖,那麼當前線程所表明的節點必定處於同步隊列的隊首,且thread屬性爲null,至於爲何要將其設置爲null,這是AQS特地設計的。 |
int waitStatus | 當前線程的等待狀態,有5種取值。0表示初始值,1表示線程被取消,-1表示當前線程處於等待狀態,-2表示節點處於等待隊列中,-3表示下一次共享式同步狀態獲取將會無條件地被傳播下去 |
Node nextWaiter | 等待隊列中,該節點的下一個節點 |
prev
屬性和next
屬性就構成了一個雙向鏈表,也就是AQS中的同步隊列,可是想要經過這個隊列找到隊列中的每個元素,咱們就須要知道這個隊列的頭結點是誰,尾結點是誰。所以AQS中又提供了兩個屬性:head
和tail
,這兩個屬性的類型均是Node類型,它們分別指向同步隊列中的頭結點和尾結點。這樣AQS就能經過head和tail,找到隊列中的每個元素。同步隊列的結構示意圖以下。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()
方法便可。首節點的設置的示意圖以下。