作者:小傅哥 博客:https://
沉淀,、分享,、成長,讓自己和他人都能有所收獲!😄
一,、前言
如果你相信你做什么都能成,你會自信的多!
千萬不要總自我否定,尤其是職場的打工人,。如果你經(jīng)常感覺,這個(gè)做不好,那個(gè)學(xué)不會,別的也不懂,那么久而久之會越來越缺乏自信。
一般說能成事的人都具有賭徒
精神,在他們眼里只要做這事那就一定能成,當(dāng)然也有可能最后就沒成,但在整個(gè)過程中人的心態(tài)是良好的,每天都有一個(gè)飽滿的精神狀態(tài),孜孜不倦的奮斗著,。最后也就是這樣的斗志讓走在一個(gè)起點(diǎn)的小伙伴,有了差距,。
二、面試題
謝飛機(jī),小記
,今天打工人呀,明天早上困呀,嘟嘟嘟,喂?誰呀,打農(nóng)藥呢!?
謝飛機(jī) :哎呦,面試官大哥,咋了!
面試官 :偷偷告訴你哈,你一面過了,。
謝飛機(jī) :嘿嘿,真的呀!太好了!哈哈哈,那我還準(zhǔn)備點(diǎn)什么呢!?
面試官 :二面會比較難嘍,嗯,我順便問你一個(gè)哈,。AQS 你了解嗎,ReentrantLock 獲取鎖的過程是什么樣的?什么是 CAS?…
謝飛機(jī) :我我我,腦子還在后羿射箭里 ,我一會就看看!!
面試官 :好好準(zhǔn)備下吧,打工人,打工魂!
三、ReentrantLock 和 AQS
1. ReentrantLock 知識鏈
ReentrantLock 可重入獨(dú)占鎖涉及的知識點(diǎn)較多,為了更好的學(xué)習(xí)這些知識,在上一章節(jié)先分析源碼和學(xué)習(xí)實(shí)現(xiàn)了公平鎖的幾種方案,。包括:CLH,、MCS、Ticket,通過這部分內(nèi)容的學(xué)習(xí),再來理解 ReentrantLock 中關(guān)于 CLH 的變體實(shí)現(xiàn)和相應(yīng)的應(yīng)用就比較容易了,。
接下來沿著 ReentrantLock 的知識鏈,繼續(xù)分析 AQS 獨(dú)占鎖的相關(guān)知識點(diǎn),如圖 17-1
在這部分知識學(xué)習(xí)中,會主要圍繞 ReentrantLock 中關(guān)于 AQS 的使用進(jìn)行展開,逐步分析源碼了解原理,。
AQS 是 AbstractQueuedSynchronizer 的縮寫,幾乎所有 Lock 都是基于 AQS 來實(shí)現(xiàn)了,其底層大量使用 CAS 提供樂觀鎖服務(wù),在沖突時(shí)采用自旋方式進(jìn)行重試,以此實(shí)現(xiàn)輕量級和高效的獲取鎖,。
另外 AbstractQueuedSynchronizer 是一個(gè)抽象類,但并沒有定義相應(yīng)的抽象方法,而是提供了可以被字類繼承時(shí)覆蓋的 protected 的方法,這樣就可以非常方便的支持繼承類的使用。
2. 寫一個(gè)簡單的 AQS 同步類
在學(xué)習(xí) ReentrantLock 中應(yīng)用的 AQS 之前,先實(shí)現(xiàn)一個(gè)簡單的同步類,來體會下 AQS 的作用,。
2.1 代碼實(shí)現(xiàn)
public class SyncLock {
private final Sync sync;
public SyncLock ( ) {
sync = new Sync ( ) ;
}
public void lock ( ) {
sync. acquire ( 1 ) ;
}
public void unlock ( ) {
sync. release ( 1 ) ;
}
private static class Sync extends AbstractQueuedSynchronizer {
@Override
protected boolean tryAcquire ( int arg) {
return compareAndSetState ( 0 , 1 ) ;
}
@Override
protected boolean tryRelease ( int arg) {
setState ( 0 ) ;
return true ;
}
// 該線程是否正在獨(dú)占資源,只有用到 Condition 才需要去實(shí)現(xiàn)
@Override
protected boolean isHeldExclusively ( ) {
return getState ( ) == 1 ;
}
}
}
這個(gè)實(shí)現(xiàn)的過程屬于 ReentrantLock 簡版,主要包括如下內(nèi)容:
Sync 類繼承 AbstractQueuedSynchronizer,并重寫方法:tryAcquire,、tryRelease、isHeldExclusively,。 這三個(gè)方法基本是必須重寫的,如果不重寫在使用的時(shí)候就會拋異常 UnsupportedOperationException
,。 重寫的過程也比較簡單,主要是使用 AQS 提供的 CAS 方法。以預(yù)期值為 0,寫入更新值 1,寫入成功則獲取鎖成功,。其實(shí)這個(gè)過程就是對 state 使用 unsafe
本地方法,傳遞偏移量 stateOffset 等參數(shù),進(jìn)行值交換操作,。unsafe.compareAndSwapInt(this, stateOffset, expect, update)
最后提供 lock、unlock 兩個(gè)方法,實(shí)際的類中會實(shí)現(xiàn) Lock 接口中的相應(yīng)方法,這里為了簡化直接自定義這樣兩個(gè)方法,。
2.2 單元測試
@Test
public void test_SyncLock ( ) throws InterruptedException {
final SyncLock lock = new SyncLock ( ) ;
for ( int i = 0 ; i < 10 ; i++ ) {
Thread. sleep ( 200 ) ;
new Thread ( new TestLock ( lock) , String. valueOf ( i) ) . start ( ) ;
}
Thread. sleep ( 100000 ) ;
}
static class TestLock implements Runnable {
private SyncLock lock;
public TestLock ( SyncLock lock) throws InterruptedException {
this . lock = lock;
}
@Override
public void run ( ) {
try {
lock. lock ( ) ;
Thread. sleep ( 1000 ) ;
System. out. println ( String. format ( "Thread %s Completed" , Thread. currentThread ( ) . getName ( ) ) ) ;
} catch ( Exception e) {
e. printStackTrace ( ) ;
} finally {
lock. unlock ( ) ;
}
}
}
以上這個(gè)單元測試和我們在上一章節(jié)介紹公平鎖時(shí)是一樣的,驗(yàn)證順序輸出,。當(dāng)然你也可以選擇多線程操作一個(gè)方法進(jìn)行加和運(yùn)算。 在測試的過程中可以嘗試把加鎖代碼注釋掉,進(jìn)行比對,。如果可以順序輸出,那么就是預(yù)期結(jié)果。
測試結(jié)果
Thread 0 Completed
Thread 1 Completed
Thread 2 Completed
Thread 3 Completed
Thread 4 Completed
Thread 5 Completed
Thread 6 Completed
Thread 7 Completed
Thread 8 Completed
Thread 9 Completed
從測試結(jié)果看,以上 AQS 實(shí)現(xiàn)的同步類,滿足預(yù)期效果,。 有了這段代碼的概念結(jié)構(gòu),接下來在分析 ReentrantLock 中的 AQS 使用就有一定的感覺了!
3. CAS 介紹
CAS 是 compareAndSet 的縮寫,它的應(yīng)用場景就是對一個(gè)變量進(jìn)行值變更,在變更時(shí)會傳入兩個(gè)參數(shù):一個(gè)是預(yù)期值,、另外一個(gè)是更新值。如果被更新的變量預(yù)期值與傳入值一致,則可以變更,。
CAS 的具體操作使用到了 unsafe
類,底層用到了本地方法 unsafe.compareAndSwapInt
比較交換方法,。
CAS 是一種無鎖算法,這種操作是 CPU 指令集操作,只有一步原子操作,速度非常快,。而且 CAS 避免了請求操作系統(tǒng)來裁定鎖問題,直接由 CPU 搞定,但也不是沒有開銷,比如 Cache Miss,感興趣的小伙伴可以自行了解 CPU 硬件相關(guān)知識,。
4. AQS 核心源碼分析
4.1 獲取鎖流程圖
圖 17-2 就是整個(gè) ReentrantLock 中獲取鎖的核心流程,包括非公平鎖和公平鎖的一些交叉流程。接下來我們就以此按照此流程來講解相應(yīng)的源碼部分,。
4.2 lock
ReentrantLock 實(shí)現(xiàn)了非公平鎖和公平鎖,所以在調(diào)用 lock.lock();
時(shí),會有不同的實(shí)現(xiàn)類:
非公平鎖,會直接使用 CAS 進(jìn)行搶占,修改變量 state 值,。如果成功則直接把自己的線程設(shè)置到 exclusiveOwnerThread,也就是獲得鎖成功。不成功后續(xù)分析 公平鎖,則不會進(jìn)行搶占,而是規(guī)規(guī)矩矩的進(jìn)行排隊(duì),。老實(shí)人
4.3 compareAndSetState
final void lock ( ) {
if ( compareAndSetState ( 0 , 1 ) )
setExclusiveOwnerThread ( Thread. currentThread ( ) ) ;
else
acquire ( 1 ) ;
}
在非公平鎖的實(shí)現(xiàn)類里,獲取鎖的過程,有這樣一段 CAS 操作的代碼,。compareAndSetState
賦值成功則獲取鎖。那么 CAS 這里面做了什么操作?
protected final boolean compareAndSetState ( int expect, int update) {
// See below for intrinsics setup to support this
return unsafe. compareAndSwapInt ( this , stateOffset, expect, update) ;
}
往下翻我們看到這樣一段代碼,這里是 unsafe 功能類的使用,兩個(gè)參數(shù)到這里變成四個(gè)參數(shù),。多了 this,、stateOffset。this 是對象本身,那么 stateOffset 是什么?
stateOffset = unsafe. objectFieldOffset
( AbstractQueuedSynchronizer. class . getDeclaredField ( "state" ) ) ;
再往下看我們找到,stateOffset 是偏移量值,偏移量是一個(gè)固定的值,。接下來我們就看看這個(gè)值到底是多少!
引用POM jol-cli
<!-- https:///artifact/org.openjdk.jol/jol-cli -->
< dependency>
< groupId> org.openjdk.jol</ groupId>
< artifactId> jol-cli</ artifactId>
< version> 0.14</ version>
</ dependency>
單元測試
@Test
public void test_stateOffset ( ) throws Exception {
Unsafe unsafe = getUnsafeInstance ( ) ;
long state = unsafe. objectFieldOffset
( AbstractQueuedSynchronizer. class . getDeclaredField ( "state" ) ) ;
System. out. println ( state) ;
}
// 16
通過 getUnsafeInstance 方法獲取 Unsafe,這是一個(gè)固定的方法,。 在獲取 AQS 類中的屬性字段 state 的偏移量,16。 除了這個(gè)屬性外你還可以拿到:headOffset,、tailOffset,、waitStatusOffset,、nextOffset,的值,最終自旋來變更這些變量的值。
4.4 (AQS)acquire
public final void acquire ( int arg) {
if ( ! tryAcquire ( arg) &&
acquireQueued ( addWaiter ( Node. EXCLUSIVE) , arg) )
selfInterrupt ( ) ;
}
整個(gè)這塊代碼里面包含了四個(gè)方法的調(diào)用,如下:
tryAcquire ,分別由繼承 AQS 的公平鎖(FairSync),、非公平鎖(NonfairSync)實(shí)現(xiàn),。addWaiter ,該方法是 AQS 的私有方法,主要用途是方法 tryAcquire 返回 false 以后,也就是獲取鎖失敗以后,把當(dāng)前請求鎖的線程添加到隊(duì)列中,并返回 Node 節(jié)點(diǎn)。acquireQueued ,負(fù)責(zé)把 addWaiter 返回的 Node 節(jié)點(diǎn)添加到隊(duì)列結(jié)尾,并會執(zhí)行獲取鎖操作以及判斷是否把當(dāng)前線程掛起,。selfInterrupt ,是 AQS 中的 Thread.currentThread().interrupt()
方法調(diào)用,它的主要作用是在執(zhí)行完 acquire 之前自己執(zhí)行中斷操作,。
4.5 tryAcquire
final boolean nonfairTryAcquire ( int acquires) {
final Thread current = Thread. currentThread ( ) ;
int c = getState ( ) ;
if ( c == 0 ) {
if ( compareAndSetState ( 0 , acquires) ) {
setExclusiveOwnerThread ( current) ;
return true ;
}
}
else if ( current == getExclusiveOwnerThread ( ) ) {
int nextc = c + acquires;
if ( nextc < 0 ) // overflow
throw new Error ( "Maximum lock count exceeded" ) ;
setState ( nextc) ;
return true ;
}
return false ;
}
這部分獲取鎖的邏輯比較簡單,主要包括兩部分:
如果 c == 0
,鎖沒有被占用,嘗試使用 CAS 方式獲取鎖,并返回 true。 如果 current == getExclusiveOwnerThread()
,也就是當(dāng)前線程持有鎖,則需要調(diào)用 setState
進(jìn)行鎖重入操作,。setState 不需要加鎖,因?yàn)槭窃谧约旱漠?dāng)前線程下,。 最后如果兩種都不滿足😌,則返回 false。
4.6 addWaiter
private Node addWaiter ( Node mode) {
Node node = new Node ( Thread. currentThread ( ) , mode) ;
Node pred = tail;
// 如果隊(duì)列不為空, 使用 CAS 方式將當(dāng)前節(jié)點(diǎn)設(shè)為尾節(jié)點(diǎn)
if ( pred != null) {
node. prev = pred;
if ( compareAndSetTail ( pred, node) ) {
pred. next = node;
return node;
}
}
// 隊(duì)列為空,、CAS失敗,將節(jié)點(diǎn)插入隊(duì)列
enq ( node) ;
return node;
}
當(dāng)執(zhí)行方法 addWaiter
,那么就是 !tryAcquire = true
,也就是 tryAcquire 獲取鎖失敗了,。 接下來就是把當(dāng)前線程封裝到 Node 節(jié)點(diǎn)中,加入到 FIFO 隊(duì)列中。因?yàn)橄冗M(jìn)先出,所以后來的隊(duì)列加入到隊(duì)尾 compareAndSetTail
不一定一定成功,因?yàn)樵诓l(fā)場景下,可能會出現(xiàn)操作失敗,。那么失敗后,則需要調(diào)用 enq 方法,該方法會自旋操作,把節(jié)點(diǎn)入隊(duì)列,。
enq
private Node enq ( final Node node) {
for ( ; ; ) {
Node t = tail;
if ( t == null) { // Must initialize
if ( compareAndSetHead ( new Node ( ) ) )
tail = head;
} else {
node. prev = t;
if ( compareAndSetTail ( t, node) ) {
t. next = node;
return t;
}
}
}
}
自旋轉(zhuǎn)for循環(huán)
+ CAS 入隊(duì)列。 當(dāng)隊(duì)列為空時(shí),則會新創(chuàng)建一個(gè)節(jié)點(diǎn),把尾節(jié)點(diǎn)指向頭節(jié)點(diǎn),然后繼續(xù)循環(huán),。 第二次循環(huán)時(shí),則會把當(dāng)前線程的節(jié)點(diǎn)添加到隊(duì)尾,。head 節(jié)是一個(gè)無用節(jié)點(diǎn),這和我們做CLH實(shí)現(xiàn)時(shí)類似
注意,從尾節(jié)點(diǎn)逆向遍歷
首先這里的節(jié)點(diǎn)連接操作并不是原子,也就是說在多線程并發(fā)的情況下,可能會出現(xiàn)個(gè)別節(jié)點(diǎn)并沒有設(shè)置 next 值,就失敗了。 但這些節(jié)點(diǎn)的 prev 是有值的,所以需要逆向遍歷,讓 prev 屬性重新指向新的尾節(jié)點(diǎn),直至全部自旋入隊(duì)列,。
4.7 acquireQueued
final boolean acquireQueued ( final Node node, int arg) {
boolean failed = true ;
try {
boolean interrupted = false ;
for ( ; ; ) {
final Node p = node. predecessor ( ) ;
// 當(dāng)前節(jié)點(diǎn)的前驅(qū)就是head節(jié)點(diǎn)時(shí), 再次嘗試獲取鎖
if ( p == head && tryAcquire ( arg) ) {
setHead ( node) ;
p. next = null; // help GC
failed = false ;
return interrupted;
}
// 獲取鎖失敗后, 判斷是否把當(dāng)前線程掛起
if ( shouldParkAfterFailedAcquire ( p, node) &&
parkAndCheckInterrupt ( ) )
interrupted = true ;
}
} finally {
if ( failed)
cancelAcquire ( node) ;
}
}
當(dāng)獲取鎖流程走到這,說明節(jié)點(diǎn)已經(jīng)加入隊(duì)列完成,。看源碼中接下來就是讓該方法再次嘗試獲取鎖,如果獲取鎖失敗會判斷是否把線程掛起,。
setHead
private void setHead ( Node node) {
head = node;
node. thread = null;
node. prev = null;
}
在學(xué)習(xí) CLH 公平鎖數(shù)據(jù)結(jié)構(gòu)中講到Head節(jié)點(diǎn)是一個(gè)虛節(jié)點(diǎn),如果當(dāng)前節(jié)點(diǎn)的前驅(qū)節(jié)點(diǎn)是Head節(jié)點(diǎn),那么說明此時(shí)Node節(jié)點(diǎn)排在隊(duì)列最前面,可以嘗試獲取鎖,。
獲取鎖后設(shè)置Head節(jié)點(diǎn),這個(gè)過程就是一個(gè)出隊(duì)列過程,原來節(jié)點(diǎn)設(shè)置Null方便GC。
shouldParkAfterFailedAcquire
private static boolean shouldParkAfterFailedAcquire ( Node pred, Node node) {
int ws = pred. waitStatus;
if ( ws == Node. SIGNAL)
// SIGNAL 設(shè)置了前一個(gè)節(jié)點(diǎn)完結(jié)喚醒,安心干別的去了,這里是睡,。
return true ;
if ( ws > 0 ) {
do {
node. prev = pred = pred. prev;
} while ( pred. waitStatus > 0 ) ;
pred. next = node;
} else {
compareAndSetWaitStatus ( pred, ws, Node. SIGNAL) ;
}
return false ;
}
你是否還CANCELLED,、SIGNAL、CONDITION ,、PROPAGATE ,這四種狀態(tài),在這個(gè)方法中用到了兩種如下:
CANCELLED ,取消排隊(duì),放棄獲取鎖,。SIGNAL ,標(biāo)識當(dāng)前節(jié)點(diǎn)的下一個(gè)節(jié)點(diǎn)狀態(tài)已經(jīng)被掛起,意思就是大家一起排隊(duì)上廁所,隊(duì)伍太長了,后面的謝飛機(jī)說,我去買個(gè)油條哈,一會到我了,你微信我哈。其實(shí)就是當(dāng)前線程執(zhí)行完畢后,需要額外執(zhí)行喚醒后繼節(jié)點(diǎn)操作,。
那么 ,以上這段代碼主要的執(zhí)行內(nèi)容包括:
如果前一個(gè)節(jié)點(diǎn)狀態(tài)是 SIGNAL
,則返回 true,。安心睡覺😪等著被叫醒 如果前一個(gè)節(jié)點(diǎn)狀態(tài)是 CANCELLED
,就是它放棄了,則繼續(xù)向前尋找其他節(jié)點(diǎn)。 最后如果什么都沒找到,就給前一個(gè)節(jié)點(diǎn)設(shè)置個(gè)鬧鐘 SIGNAL
,等著被通知,。
4.8 parkAndCheckInterrupt
if ( shouldParkAfterFailedAcquire ( p, node) &&
parkAndCheckInterrupt ( ) )
interrupted = true ;
// 線程掛起等待被喚醒
private final boolean parkAndCheckInterrupt ( ) {
LockSupport. park ( this ) ;
return Thread. interrupted ( ) ;
}
當(dāng)方法 shouldParkAfterFailedAcquire
返回 false 時(shí),則執(zhí)行 parkAndCheckInterrupt() 方法,。 那么,這一段代碼就是對線程的掛起操作,LockSupport.park(this);
。 Thread.interrupted()
檢查當(dāng)前線程的中斷標(biāo)識,。
四,、總結(jié)
ReentrantLock 的知識比較多,涉及的代碼邏輯也比較復(fù)雜,在學(xué)習(xí)的過程中需要對照源碼和相關(guān)并發(fā)書籍和資料一起學(xué)習(xí),以及最好的是自身實(shí)踐,。 AQS 的實(shí)現(xiàn)部分涉及的內(nèi)容較多,例如:state 屬性使用 unsafe 提供的本地方法進(jìn)行 CAS 操作,把初始值 0 改為 1,則獲得了鎖。addWaiter,、acquireQueued,、shouldParkAfterFailedAcquire、parkAndCheckInterrupt等,可以細(xì)致總結(jié),。 所有的 Lock 都是基于 AQS 來實(shí)現(xiàn)了,。AQS 和 Condition 各自維護(hù)了不同的隊(duì)列,在使用 Lock 和 Condition 的時(shí)候,就是兩個(gè)隊(duì)列的互相移動。這句話可以細(xì)細(xì)體會,。可能文中會有一些不準(zhǔn)確或者錯(cuò)字,歡迎留言,我會不斷的更新博客,。
五、系列推薦
volatile 怎么實(shí)現(xiàn)的內(nèi)存可見?沒有 volatile 一定不可見嗎? synchronized 解毒,剖析源碼深度分析! ReentrantLock之公平鎖講解和實(shí)現(xiàn) 咋嘞?你的IDEA過期了吧!加個(gè)Jar包就破解了,為什么? 大學(xué)四年到畢業(yè)工作5年的學(xué)習(xí)路線資源匯總