JUC AQS ReentrantLock源碼分析

Java的內置鎖一直都是備受爭議的,在JDK

1.6之前,synchronized這個重量級鎖其性能一直都是較為低下,雖然在1.6後,進行大量的鎖優化策略,但是與Lock相比synchronized還是存在一些缺陷的:雖然synchronized提供了便捷性的隱式獲取鎖釋放鎖機制(基於JVM機制),但是它卻缺少了獲取鎖與釋放鎖的可操作性,可中斷、超時獲取鎖,且它為獨佔式在高併發場景下性能大打折扣。

如何自己來實現一個同步

自旋實現一個同步

JUC AQS ReentrantLock源碼分析

圖1

缺點:耗費cpu資源。沒有競爭到鎖的線程會一直佔用cpu資源進行cas操作,假如一個線程獲得鎖後要花費Ns處理業務邏輯,那另外一個線程就會白白的花費Ns的cpu資源
解決思路:讓得不到鎖的線程讓出CPU

yield+自旋實現同步

JUC AQS ReentrantLock源碼分析

圖2

要解決自旋鎖的性能問題必須讓競爭鎖失敗的線程不空轉,而是在獲取不到鎖的時候能把cpu資源給讓出來,yield()方法就能讓出cpu資源,當線程競爭鎖失敗時,會調用yield方法讓出cpu。

自旋+yield的方式並沒有完全解決問題,當系統只有兩個線程競爭鎖時,yield是有效的。需要注意的是該方法只是當前讓出cpu,有可能操作系統下次還是選擇運行該線程,比如裡面有2000個線程,想想會有什麼問題?

sleep+自旋方式實現同步

JUC AQS ReentrantLock源碼分析

圖3

缺點:sleep的時間為什麼是10?怎麼控制呢?很多時候就算你是調用者本身其實你也不知道這個時間是多少

park+自旋方式實現同步

JUC AQS ReentrantLock源碼分析

圖4

這種方法就比較完美,當然我寫的都偽代碼,我看看大師是如何利用這種機制來實現同步的;JDK的JUC包下面ReentrantLock類的原理就是利用了這種機制;

ReentrantLock源碼分析之上鎖過程

AQS(AbstractQueuedSynchronizer)類的設計主要代碼(具體參考源碼)

JUC AQS ReentrantLock源碼分析

圖5

AQS當中的隊列示意圖

JUC AQS ReentrantLock源碼分析

圖6

Node類的設計

JUC AQS ReentrantLock源碼分析

圖7

上鎖過程重點

鎖對象:其實就是ReentrantLock的實例對象,下文應用代碼第一行中的lock對象就是所謂的鎖

自由狀態:自由狀態表示鎖對象沒有被別的線程持有,計數器為0

計數器:再lock對象中有一個字段state用來記錄上鎖次數,比如lock對象是自由狀態則state為0,如果大於零則表示被線程持有了,當然也有重入那麼state則>1

waitStatus:僅僅是一個狀態而已;ws是一個過渡狀態,在不同方法裡面判斷ws的狀態做不同的處理,所以ws=0有其存在的必要性

tail:隊列的隊尾 head:隊列的對首 ts:第二個給lock加鎖的線程 tf:第一個給lock加鎖的線程 tc:當前給線程加鎖的線程

tl:最後一個加鎖的線程 tn:隨便某個線程

當然這些線程有可能重複,比如第一次加鎖的時候tf=tc=tl=tn

節點:就是上面的Node類的對象,裡面封裝了線程,所以某種意義上node就等於一個線程

首先一個簡單的應用

JUC AQS ReentrantLock源碼分析

圖8

公平鎖lock方法的源碼分析

JUC AQS ReentrantLock源碼分析

圖9

非公平鎖的looc方法

JUC AQS ReentrantLock源碼分析

圖10

下面給出他們的代碼執行邏輯的區別圖

JUC AQS ReentrantLock源碼分析

圖11

公平鎖的上鎖是必須判斷自己是不是需要排隊;而非公平鎖是直接進行CAS修改計數器看能不能加鎖成功;如果加鎖不成功則乖乖排隊(調用acquire);所以不管公平還是不公平;只要進到了AQS隊列當中那麼他就會排隊;一朝排隊;永遠排隊記住這點

acquire方法方法源碼分析

JUC AQS ReentrantLock源碼分析

圖12

acquire方法首先會調用tryAcquire方法,注意tryAcquire的結果做了取反

tryAcquire方法源碼分析

JUC AQS ReentrantLock源碼分析

圖13

hasQueuedPredecessors判斷是否需要排隊的源碼分析

這裡需要記住一點,整個方法如果最後返回false,則去加鎖,如果返回true則不加鎖,因為這個方法被取反了

JUC AQS ReentrantLock源碼分析

圖14

JUC AQS ReentrantLock源碼分析

圖15

JUC AQS ReentrantLock源碼分析

圖16

到此我們已經解釋完了!tryAcquire(arg)方法,為了方便我再次貼一下代碼

JUC AQS ReentrantLock源碼分析

圖17

acquireQueued(addWaiter(Node.exclusive),arg))方法解析

如果代碼能執行到這裡說tc需要排隊

需要排隊有兩種情況—換言之代碼能夠執行到這裡有兩種情況:

1、tf持有了鎖,並沒有釋放,所以tc來加鎖的時候需要排隊,但這個時候—隊列並沒有初始化

2、tn(無所謂哪個線程,反正就是一個線程)持有了鎖,那麼由於加鎖tn!=tf(tf是屬於第一種情況,我們現在不考慮tf了),所以隊列是一定被初始化了的,tc來加鎖,那麼隊列當中有人在排隊,故而他也去排隊

加鎖過程總結

如果是第一個線程tf,那麼和隊列無關,線程直接持有鎖。並且也不會初始化隊列,如果接下來的線程都是交替執行,那麼永遠和AQS隊列無關,都是直接線程持有鎖,如果發生了競爭,比如tf持有鎖的過程中T2來lock,那麼這個時候就會初始化AQS,初始化AQS的時候會在隊列的頭部虛擬一個Thread為NULL的Node,因為隊列當中的head永遠是持有鎖的那個node(除了第一次會虛擬一個,其他時候都是持有鎖的那個線程鎖封裝的node),現在第一次的時候持有鎖的是tf而tf不在隊列當中所以虛擬了一個node節點,隊列當中的除了head之外的所有的node都在park,當tf釋放鎖之後unpark某個(基本是隊列當中的第二個,為什麼是第二個呢?前面說過head永遠是持有鎖的那個node,當有時候也不會是第二個,比如第二個被cancel之後,至於為什麼會被cancel,不在我們討論範圍之內,cancel的條件很苛刻,基本不會發生)node之後,node被喚醒,假設node是t2,那麼這個時候會首先把t2變成head(sethead),在sethead方法裡面會把t2代表的node設置為head,並且把node的Thread設置為null,為什麼需要設置null?其實原因很簡單,現在t2已經拿到鎖了,node就不要排隊了,那麼node對Thread的引用就沒有意義了。所以隊列的head裡面的Thread永遠為null


分享到:


相關文章: