內容導航java
簡單解釋一下J.U.C,是JDK中提供的併發工具包, java.util.concurrent。裏面提供了不少併發編程中很經常使用的實用工具類,好比atomic原子操做、好比lock同步鎖、fork/join等。node
我想以lock做爲切入點來說解AQS,畢竟同步鎖是解決線程安全問題的通用手段,也是咱們工做中用得比較多的方式。面試
Lock API編程
Lock是一個接口,方法定義以下api
Lock的實現安全
實現Lock接口的類有不少,如下爲幾個常見的鎖實現數據結構
ReentrantLock的簡單實用多線程
如何在實際應用中使用ReentrantLock呢?咱們經過一個簡單的demo來演示一下架構
這段代碼主要作一件事,就是經過一個靜態的 incr()方法對共享變量 count作連續遞增,在沒有加同步鎖的狀況下多線程訪問這個方法必定會存在線程安全問題。因此用到了 ReentrantLock來實現同步鎖,而且在finally語句塊中釋放鎖。那麼我來引出一個問題,你們思考一下併發
多個線程經過lock競爭鎖時,當競爭失敗的鎖是如何實現等待以及被喚醒的呢?
aqs全稱爲AbstractQueuedSynchronizer,它提供了一個FIFO隊列,能夠當作是一個用來實現同步鎖以及其餘涉及到同步功能的核心組件,常見的有:ReentrantLock、CountDownLatch等。
AQS是一個抽象類,主要是經過繼承的方式來使用,它自己沒有實現任何的同步接口,僅僅是定義了同步狀態的獲取以及釋放的方法來提供自定義的同步組件。
能夠這麼說,只要搞懂了AQS,那麼J.U.C中絕大部分的api都能輕鬆掌握。
AQS的兩種功能
從使用層面來講,AQS的功能分爲兩種:獨佔和共享
ReentrantLock的類圖
仍然以ReentrantLock爲例,來分析AQS在重入鎖中的使用。畢竟單純分析AQS沒有太多的含義。先理解這個類圖,能夠方便咱們理解AQS的原理
AQS的內部實現
AQS的實現依賴內部的同步隊列,也就是FIFO的雙向隊列,若是當前線程競爭鎖失敗,那麼AQS會把當前線程以及等待狀態信息構形成一個Node加入到同步隊列中,同時再阻塞該線程。當獲取鎖的線程釋放鎖之後,會從隊列中喚醒一個阻塞的節點(線程)。
AQS隊列內部維護的是一個FIFO的雙向鏈表,這種結構的特色是每一個數據結構都有兩個指針,分別指向直接的後繼節點和直接前驅節點。因此雙向鏈表能夠從任意一個節點開始很方便的訪問前驅和後繼。每一個Node實際上是由線程封裝,當線程爭搶鎖失敗後會封裝成Node加入到ASQ隊列中去
Node類的組成以下
添加節點
當出現鎖競爭以及釋放鎖的時候,AQS同步隊列中的節點會發生變化,首先看一下添加節點的場景。
這裏會涉及到兩個變化
釋放鎖移除節點
head節點表示獲取鎖成功的節點,當頭結點在釋放同步狀態時,會喚醒後繼節點,若是後繼節點得到鎖成功,會把本身設置爲頭結點,節點的變化過程以下
這個過程也是涉及到兩個變化
這裏有一個小的變化,就是設置head節點不須要用CAS,緣由是設置head節點是由得到鎖的線程來完成的,而同步鎖只能由一個線程得到,因此不須要CAS保證,只須要把head節點設置爲原首節點的後繼節點,而且斷開原head節點的next引用便可
清楚了AQS的基本架構之後,咱們來分析一下AQS的源碼,仍然以ReentrantLock爲模型。
ReentrantLock的時序圖
調用ReentrantLock中的lock()方法,源碼的調用過程我使用了時序圖來展示
從圖上能夠看出來,當鎖獲取失敗時,會調用addWaiter()方法將當前線程封裝成Node節點加入到AQS隊列,基於這個思路,咱們來分析AQS的源碼實現
分析源碼
ReentrantLock.lock()
這個是獲取鎖的入口,調用sync這個類裏面的方法,sync是什麼呢?
sync是一個靜態內部類,它繼承了AQS這個抽象類,前面說過AQS是一個同步工具,主要用來實現同步控制。咱們在利用這個工具的時候,會繼承它來實現同步控制功能。
經過進一步分析,發現Sync這個類有兩個具體的實現,分別是 NofairSync(非公平鎖), FailSync(公平鎖).
公平鎖和非公平鎖的實現上的差別,我會在文章後面作一個解釋,接下來的分析仍然以 非公平鎖做爲主要分析邏輯。
NonfairSync.lock
這段代碼簡單解釋一下
compareAndSetStatecompareAndSetState的代碼實現邏輯以下
這段代碼其實邏輯很簡單,就是經過cas樂觀鎖的方式來作比較並替換。上面這段代碼的意思是,若是當前內存中的state的值和預期值expect相等,則替換爲update。更新成功返回true,不然返回false.這個操做是原子的,不會出現線程安全問題,這裏面涉及到Unsafe這個類的操做,一級涉及到state這個屬性的意義。stateAQS中有一個這樣的屬性定義,這個對於重入鎖的實現來講,表示一個同步狀態。它有兩個含義的表示
private volatile int state;
須要注意的是:不一樣的AQS實現,state所表達的含義是不同的。UnsafeUnsafe類是在sun.misc包下,不屬於Java標準。可是不少Java的基礎類庫,包括一些被普遍使用的高性能開發庫都是基於Unsafe類開發的,好比Netty、Hadoop、Kafka等;Unsafe可認爲是Java中留下的後門,提供了一些低層次操做,如直接內存訪問、線程調度等
public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);
這個是一個native方法, 第一個參數爲須要改變的對象,第二個爲偏移量(即以前求出來的headOffset的值),第三個參數爲期待的值,第四個爲更新後的值整個方法的做用是若是當前時刻的值等於預期值var4相等,則更新爲新的指望值 var5,若是更新成功,則返回true,不然返回false;
acquire
acquire是AQS中的方法,若是CAS操做未能成功,說明state已經不爲0,此時繼續acquire(1)操做,這裏你們思考一下,acquire方法中的1的參數是用來作什麼呢?若是沒猜中,往前面回顧一下state這個概念
這個方法的主要邏輯是
NonfairSync.tryAcquire
這個方法的做用是嘗試獲取鎖,若是成功返回true,不成功返回false
它是重寫AQS類中的tryAcquire方法,而且你們仔細看一下AQS中tryAcquire方法的定義,並無實現,而是拋出異常。按照通常的思惟模式,既然是一個不實現的模版方法,那應該定義成abstract,讓子類來實現呀?你們想一想爲何
nonfairTryAcquire
tryAcquire(1)在NonfairSync中的實現代碼以下
addWaiter
當tryAcquire方法獲取鎖失敗之後,則會先調用addWaiter將當前線程封裝成Node,而後添加到AQS隊列
enq
enq就是經過自旋操做把當前節點加入到隊列中
假若有兩個線程t1,t2同時進入enq方法,t==null表示隊列是首次使用,須要先初始化
另一個線程cas失敗,則進入下次循環,經過cas操做將node添加到隊尾
到目前爲止,經過addwaiter方法構造了一個AQS隊列,而且將線程添加到了隊列的節點中
acquireQueued
將添加到隊列中的Node做爲參數傳入acquireQueued方法,這裏面會作搶佔鎖的操做
前面的邏輯都很好理解,主要看一下shouldParkAfterFailedAcquire這個方法和parkAndCheckInterrupt的做用
shouldParkAfterFailedAcquire
從上面的分析能夠看出,只有隊列的第二個節點能夠有機會爭用鎖,若是成功獲取鎖,則此節點晉升爲頭節點。對於第三個及之後的節點,if (p == head)條件不成立,首先進行shouldParkAfterFailedAcquire(p, node)操做
shouldParkAfterFailedAcquire方法是判斷一個爭用鎖的線程是否應該被阻塞。它首先判斷一個節點的前置節點的狀態是否爲Node.SIGNAL,若是是,是說明此節點已經將狀態設置-若是鎖釋放,則應當通知它,因此它能夠安全的阻塞了,返回true。
parkAndCheckInterrupt
若是shouldParkAfterFailedAcquire返回了true,則會執行: parkAndCheckInterrupt()方法,它是經過LockSupport.park(this)將當前線程掛起到WATING狀態,它須要等待一箇中斷、unpark方法來喚醒它,經過這樣一種FIFO的機制的等待,來實現了Lock的操做。
LockSupportLockSupport類是Java6引入的一個類,提供了基本的線程同步原語。LockSupport其實是調用了Unsafe類裏的函數,歸結到Unsafe裏,只有兩個函數:
unpark函數爲線程提供「許可(permit)」,線程調用park函數則等待「許可」。這個有點像信號量,可是這個「許可」是不能疊加的,「許可」是一次性的。permit至關於0/1的開關,默認是0,調用一次unpark就加1變成了1.調用一次park會消費permit,又會變成0。 若是再調用一次park會阻塞,由於permit已是0了。直到permit變成1.這時調用unpark會把permit設置爲1.每一個線程都有一個相關的permit,permit最多隻有一個,重複調用unpark不會累積
ReentrantLock.unlock
加鎖的過程分析完之後,再來分析一下釋放鎖的過程,調用release方法,這個方法裏面作兩件事,1,釋放鎖 ;2,喚醒park的線程
tryRelease
這個動做能夠認爲就是一個設置鎖狀態的操做,並且是將狀態減掉傳入的參數值(參數是1),若是結果狀態爲0,就將排它鎖的Owner設置爲null,以使得其它的線程有機會進行執行。
在排它鎖中,加鎖的時候狀態會增長1(固然能夠本身修改這個值),在解鎖的時候減掉1,同一個鎖,在能夠重入後,可能會被疊加爲二、三、4這些值,只有unlock()的次數與lock()的次數對應纔會將Owner線程設置爲空,並且也只有這種狀況下才會返回true。
unparkSuccessor
在方法unparkSuccessor(Node)中,就意味着真正要釋放鎖了,它傳入的是head節點(head節點是佔用鎖的節點),當前線程被釋放以後,須要喚醒下一個節點的線程
經過這篇文章基本將AQS隊列的實現過程作了比較清晰的分析,主要是基於非公平鎖的獨佔鎖實現。在得到同步鎖時,同步器維護一個同步隊列,獲取狀態失敗的線程都會被加入到隊列中並在隊列中進行自旋;移出隊列(或中止自旋)的條件是前驅節點爲頭節點且成功獲取了同步狀態。在釋放同步狀態時,同步器調用tryRelease(int arg)方法釋放同步狀態,而後喚醒頭節點的後繼節點。
若是對java微服務、分佈式、高併發、高可用、大型互聯網架構技術、面試經驗交流。 能夠加我架構圈子羣:142019080 領取資料,羣內天天更新資料,免費領取。