學Java的竟然有人不會AQS機制
Java中的并發包大家應該都或多或少的了解過,說到并發包也就不得不提我們今天要說的AbstractQueuedSynchronizer,簡稱AQS,這個是很多并發工具類的實現基礎
- public abstract class AbstractQueuedSynchronizer
- extends AbstractOwnableSynchronizer
- implements java.io.Serializable
類如其名,抽象的隊列式的同步器,AQS定義了一套多線程訪問共享資源的同步器框架,許多同步類實現都依賴于它,如常用的ReentrantLock、Semaphore、CountDownLatch
深入探究AQS
先來看這個圖,圖中有顏色的為Method,無顏色的為Attribution
總的來說,AQS框架共分為五層,自上而下由淺入深,從AQS對外暴露的API到底層基礎數據
當有自定義同步器接入時,只需重寫第一層所需要的部分方法即可,不需要關注底層具體的實現流程
道理也很簡單,就像我們說的,這個東西是一個抽象的同步器,它將加鎖和解鎖這些操作交給了具體的實現類來自己實現,就像這樣
當自定義同步器進行加鎖或者解鎖操作時,先經過第一層的API進入AQS內部方法,然后經過第二層進行鎖的獲取,獲取鎖成功之后便直接執行相應的邏輯,對于獲取鎖失敗的流程,進入第三層和第四層的等待隊列處理,而這些處理方式均依賴于第五層的基礎數據提供層
這樣給大家說的話,應該很容易就可以理解了
AQS的實現數據結構
研究過AQS的同學應該對這個圖都很熟悉了,AQS的核心就是state+Node+CLH變體雙向隊列
核心思想就是通過一個volatile類型state狀態來表示共享資源的狀態,如果被請求的資源空閑,就將獲得共享資源的線程設置為當前有效的線程,然后修改state為鎖定狀態,其它的線程及時可見
共享資源被占用之后,其它線程肯定不能直接就返回失敗啊,這樣這個并發包的高效就沒得了,所以就引入了一個雙向隊列,這個雙向等待隊列放置那些暫時還未搶到共享資源的線程,來完成等待喚醒機制
實際上,AQS的運行中的這個CLH變體的雙向隊列,不知存儲未搶到共享資源的線程,而搶到共享資源的這個線程也會作為隊列的頭節點head存在
CLH:Craig、Landin and Hagersten隊列,是單向鏈表,AQS中的隊列是CLH變體的虛擬雙向隊列(FIFO),AQS是通過將每條請求共享資源的線程封裝成一個節點來實現鎖的分配。
這么說大家應該就很容易懂了吧,就是大家一起搶共享資源,搶到的就是有效線程,放到雙向隊列的head頭節點,沒搶到的就依次往后排
我們接著看一下Node節點是怎么做的
這個是Node節點的屬性值和含義
簡單解釋一下,waitStatus就是節點在隊列中的狀態,Thread就是當前節點的線程,prev和next是前驅指針和后繼指針
這里的重點就是waitStatus屬性
CANCELLED(1):表示當前結點已取消調度。當timeout或被中斷(響應中斷的情況下),會觸發變更為此狀態,進入該狀態后的結點將不會再變化。
SIGNAL(-1):表示后繼結點在等待當前結點喚醒。后繼結點入隊時,會將前繼結點的狀態更新為SIGNAL。
CONDITION(-2):表示結點等待在Condition上,當其他線程調用了Condition的signal()方法后,CONDITION狀態的結點將從等待隊列轉移到同步隊列中,等待獲取同步鎖。
PROPAGATE(-3):共享模式下,前繼結點不僅會喚醒其后繼結點,同時也可能會喚醒后繼的后繼結點。
0:新結點入隊時的默認狀態。
正是由于這個特點,負值表示結點處于有效等待狀態,而正值表示結點已被取消。所以源碼中很多地方用>0、<0來判斷結點的狀態是否正常
同步狀態state
AQS中維護了一個名為state的字段,意為同步狀態,是由Volatile修飾的,用于展示當前臨界資源的獲鎖情況。
- private volatile int state;
對于這個state,AQS也是提供了幾個方法
這幾個方法都是final類型的,子類是無法修改的
在AQS中的是有兩種加鎖模式的,一種是共享式,一種是獨占式,共享式也很簡單,就是通過控制AQS中的state數值即可
state是AQS中的volatile類型,具有可見性,用于記錄加鎖狀態和重入的次數,當然不只是重入次數,其實這個state在不同的實現類中是有不同的意義的
【ReentrantLock】:state用于記錄鎖的持有狀態和重入次數,state=0表示沒有線程持有鎖;state=1表示有一個線程持有鎖;state=N表示exclusiveOwnerThread這個線程N次重入了這個鎖。
【ReentrantReadWriteLock】:state用于記錄讀寫鎖的占用狀態和持有線程數量(讀鎖)、重入次數(寫鎖),state的高16位記錄持有讀鎖的線程數量,低16位記錄寫鎖線程重入次數,如果這16位的值是0,表示沒有線程占用鎖,否則表示有線程持有鎖。
另外針對讀鎖,每個線程獲取到的讀鎖次數由本地線程變量中的HoldCounter記錄。
【Semaphore】:state用于計數。state=N表示還有N個信號量可以分配出去,state=0表示沒有信號量了,此時所有需要acquire信號量的線程都等著;
【CountDownLatch】:state也用于計數,每次countDown都減一,減到0的時候喚醒被await阻塞的線程。
切記:區分開volatile類型的state屬性和Node節點中的waitStatus屬性
搶占共享資源也是有兩種方式的:公平鎖和非公平鎖
大家用過ReentrantLock的同學肯定都知道,默認的是非公平鎖,但是我們可以傳入一個參數設置為公平鎖
按照ReentrantLock來說一下公平鎖和非公平鎖
公平鎖,是公平的,可以保證獲取鎖的線程按照先來后到的順序,獲取到鎖。
非公平鎖,各個線程獲取到鎖的順序,不一定和它們申請的先后順序一致,有可能后來的線程,反而先獲取到了鎖。
在實現上,公平鎖在進行lock時,首先會進行tryAcquire()操作。
在tryAcquire中,會判斷等待隊列中是否已經有別的線程在等待了。如果隊列中已經有別的線程了,則tryAcquire失敗,則將自己加入隊列。
如果隊列中沒有別的線程,則進行獲取鎖的操作。
非公平鎖,在進行lock時,會直接嘗試進行加鎖,如果成功,則獲取到鎖,如果失敗,則進行和公平鎖相同的動作。
從公平鎖和非公平的實現上來看,他們的操作基本相同,唯一的區別在于,在lock時,非公平鎖會直接先進行嘗試加鎖的操作。
當前一個線程完成了鎖的使用,并且釋放了,而且此時等待隊列非空時,如果這是有新線程申請鎖,那么,公平鎖和非公平鎖的表現就會出現差異。
公平鎖
優點:線程按照順序獲取鎖,不會出現餓死現象(注:餓死現象是指一個線程的CPU執行時間都被其他線程占用,導致得不到CPU執行。
缺點:整體吞吐效率相對非公平鎖要低,等待隊列中除一個線程以外的所有線程都會阻塞,CPU喚醒線程的開銷比非公平鎖要大。
非公平鎖
優點:可以減少喚起線程上下文切換的消耗,整體吞吐量比公平鎖高。
缺點:在高并發環境下可能造成線程優先級反轉和餓死現象。
AQS作為并發編程的框架,為很多其他同步工具提供了良好的解決方案。下面列出了JUC中的幾種同步工具,大體介紹一下AQS的應用場景: