教你Java中的Lock鎖底層AQS到底是如何實現(xiàn)的
前言
相信大家對Java中的Lock鎖應(yīng)該不會陌生,比如ReentrantLock,鎖主要是用來解決解決多線程運行訪問共享資源時的線程安全問題。那你是不是很好奇,這些Lock鎖api是如何實現(xiàn)的呢?本文就是來探討一下這些Lock鎖底層的AQS(AbstractQueuedSynchronizer)到底是如何實現(xiàn)的。
本文是基于ReentrantLock來講解,ReentrantLock加鎖只是對AQS的api的調(diào)用,底層的鎖的狀態(tài)(state)和其他線程等待(Node雙向鏈表)的過程其實是由AQS來維護的
加鎖
我們先來看看加鎖的過程,先看源碼,然后模擬兩個線程來加鎖的過程。
上圖是ReentrantLock的部分實現(xiàn)。里面有一個Sync的內(nèi)部類的實例變量,這個Sync內(nèi)部類繼承自AQS,Sync子類就包括公平鎖和非公平鎖的實現(xiàn)。說白了其實ReentrantLock是通過Sync的子類來實現(xiàn)加鎖。
我們就來看一下Sync的非公平鎖的實現(xiàn)NonfairSync。
重寫了它的lock加鎖方法,在實現(xiàn)中因為是非公平的,所以一進來會先通過cas嘗試將AQS類的state參數(shù)改為1,直接嘗試加鎖。如果嘗試加鎖失敗會調(diào)用AQS的acquire方法繼續(xù)嘗試加鎖。
假設(shè)這里有個線程1先來調(diào)用lock方法,那么此時沒有人加鎖,那么就通過CAS操作,將AQS中的state中的變量由0改為1,代表有人來加鎖,然后將加鎖的線程設(shè)置為自己如圖。
那么此時有另一個線程2來加鎖,發(fā)現(xiàn)通過CAS操作會失敗,因為state已經(jīng)被設(shè)置為1了,線程線程2就會設(shè)置失敗,那么此時就會走else,調(diào)用AQS的acquire方法繼續(xù)嘗試加鎖。
進入到acquire會先調(diào)用tryAcquire再次嘗試加鎖,而這個tryAcquire方法AQS其實是沒有什么實現(xiàn)的,會調(diào)用到NonfairSync里面的tryAcquire,而tryAcquire實際會調(diào)用到Sync內(nèi)部類里面的nonfairTryAcquire非公平嘗試加鎖方法。
先獲取鎖的狀態(tài),判斷鎖的狀態(tài)是不是等于0,等于0說明沒人加鎖,可以嘗試去加,如果被加鎖了,就會走else if,else if會判斷加鎖的線程是不是當前線程,是的話就給state 加 1,代表當前線程加了2次鎖,就是可重入鎖的意思(所謂的可重入就是代表一個線程可以多次獲取到鎖,只是將state 設(shè)置為多次,當線程多次釋放鎖之后,將state 設(shè)置為0才代表當前線程完全釋放了鎖)。
這里所有的條件假設(shè)都不成立。也就是線程2嘗試加鎖的時候,線程1并沒有釋放鎖,那么這個方法就會返回false。
接下來就會走到addWaiter方法,這個方法很重要,就是將當前線程封裝成一個Node,然后將這個Node放入雙向鏈表中。addWaiter先根據(jù)指定模式創(chuàng)建指定的node節(jié)點,因為ReentrantLock是獨占模式,所以傳進去的EXCLUSIVE,這里通過當前線程和模式傳入,初始化一個雙向node節(jié)點,獲取最后一個節(jié)點,根據(jù)最后一個節(jié)點是否存在來操作當前節(jié)點的父級。如果尾節(jié)點不存在會去調(diào)用enq去初始化
放入鏈表中之后如圖。
然后調(diào)用acquireQueued方法
這個方法一進來也會嘗試將當前節(jié)點去加鎖,然后如果加鎖成功就將當前節(jié)點設(shè)置為頭節(jié)點,最后將當前線程中斷,等待喚醒。
線程2進來的時候,剛好線程2的前一個節(jié)點是頭節(jié)點,但是不巧的是調(diào)用tryAcquire方法,還是失敗,那么此時就會走shouldParkAfterFailedAcquire方法,這個方法是在線程休眠之前調(diào)用的,很重要,我們來看看干了什么事。
判斷當前節(jié)點的父級節(jié)點的狀態(tài),如果父級狀態(tài)是-1,則代表當前線程可以被喚醒了。如果父級的狀態(tài)為取消狀態(tài)(什么叫非取消狀態(tài),就是tryLock方法等待了一些時間沒獲取到鎖的線程就處于取消狀態(tài))就跳過父級,尋找下一個可以被喚醒的父級,然后綁定上節(jié)點關(guān)系,最后將父級的狀態(tài)更改為-1。也就說,線程(Node)加入隊列之后,如果沒有獲取到鎖,在睡眠之前,會將當前節(jié)點的前一個節(jié)點設(shè)置為非取消狀態(tài)的節(jié)點,然后將前一個節(jié)點的waitStatus設(shè)置為-1,代表前一個節(jié)點在釋放鎖的時候需要喚醒下一個節(jié)點。這一步驟主要是防止當前休眠的線程無法被喚醒。這一切設(shè)置成功之后,就會返回true。
接下來就會調(diào)用parkAndCheckInterrupt
,這個方法內(nèi)部調(diào)用LockSupport.park方法,此時當前線程就會休眠。
到這一步線程2由于沒有獲取到鎖,就會在這里休眠等待被喚醒。
來總結(jié)一下加鎖的過程。
線程1先過來,發(fā)現(xiàn)沒人加鎖,那么此時就會加上鎖。此時線程2過來,在線程2加鎖的過程中,線程1始終沒有釋放鎖,那么線程2就不會加鎖成功(如果在線程2加鎖的過程中線程1始終釋放鎖,那么線程2就會加鎖成功),線程2沒有加鎖成功,就會將自己當前線程加入等待隊列中(如果沒有隊列就先初始化一個),然后設(shè)置前一個節(jié)點的狀態(tài),最后通過LockSupport.park方法,將自己這個線程休眠。
如果后面還有線程3,線程4等等諸多的先過來,那么這些線程都會按照前面線程2的步驟,將自己插入鏈表后面再休眠。
釋放鎖
ok,說完加鎖的過程之后,我們來看看釋放鎖干了什么。
ReentrantLock的unlock其實是調(diào)用AQS的release方法,我們直接進入release方法,看看是如何實現(xiàn)的
進入tryRelease方法,看一下Sync的實現(xiàn)
其實很簡單,就是判斷鎖的狀態(tài),也就是加了幾次鎖,然后減去釋放的,最后判斷釋放之后,鎖的狀態(tài)是不是0(因為可能線程加了多次鎖,所以得判斷一下),是的話說明當前這個鎖已經(jīng)釋放完了,然后將占有鎖的線程設(shè)置為null,然后返回true,
然后就會走接下來的代碼。
就是判斷當前鏈表頭節(jié)點是不是需要喚醒隊列中的線程。如果有鏈表的話,頭結(jié)點的waitStatus肯定不是0,因為線程休眠之前,會將前一個節(jié)點的狀態(tài)設(shè)置為-1,上面加鎖的過程中有提到過。
接下來就會走unparkSuccessor方法,successor代表繼承者的意思,見名知意,這個方法其實就會喚醒當前線程中離頭節(jié)點最近的沒有狀態(tài)為非取消的線程。然后調(diào)用LockSupport.unpark,喚醒等待的線
然后線程就會從阻塞的那里蘇醒過來,繼續(xù)嘗試獲取鎖。
我再次貼出這段代碼。
獲取到鎖之后,就將頭節(jié)點設(shè)置成自己。
對應(yīng)我們的例子,就是線程1釋放鎖之后,就會喚醒在隊列中線程2,先成2獲取到鎖之后,就會將自己前一個節(jié)點(也就是頭節(jié)點)從鏈表中移除,將自己設(shè)置成頭節(jié)點。該方法就會跳出死循環(huán)。
到這里,釋放鎖的過程就講完了,其實很簡單,就是當線程完完全全釋放了鎖,會喚醒當前鏈表中的沒有取消的,離頭結(jié)點最近的節(jié)點(一般就是鏈表中的第二個節(jié)點),然后被喚醒的節(jié)點就會獲取到鎖,將頭節(jié)點設(shè)置為自己。
總結(jié)
相信看完這篇文章,大家對AQS的底層有了更深層次的了解。AQS其實就是內(nèi)部維護一個鎖的狀態(tài)變量state和一個雙向鏈表,加鎖成功就將state的值加1,加鎖失敗就將自己當前線程放入鏈表的尾部,然后休眠,等待其他線程完完全全釋放鎖之后將自己喚醒,喚醒之后會嘗試加鎖,加鎖成功就會執(zhí)行業(yè)務(wù)代碼了。
到此這篇關(guān)于教你Java中的Lock鎖底層AQS到底是如何實現(xiàn)的的文章就介紹到這了,更多相關(guān)Java Lock鎖AQS實現(xiàn)內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Spring?Cloud?使用?Resilience4j?實現(xiàn)服務(wù)熔斷的方法
服務(wù)熔斷是為了保護我們的服務(wù),比如當某個服務(wù)出現(xiàn)問題的時候,控制打向它的流量,讓它有時間去恢復(fù),或者限制一段時間只能有固定數(shù)量的請求打向這個服務(wù),這篇文章主要介紹了Spring?Cloud?使用?Resilience4j?實現(xiàn)服務(wù)熔斷,需要的朋友可以參考下2022-12-12java 網(wǎng)絡(luò)編程之TCP通信和簡單的文件上傳功能實例
下面小編就為大家分享一篇java 網(wǎng)絡(luò)編程之TCP通信和簡單的文件上傳功能實例,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2018-01-01使用java將動態(tài)網(wǎng)頁生成靜態(tài)網(wǎng)頁示例
這篇文章主要介紹了使用java將動態(tài)網(wǎng)頁生成靜態(tài)網(wǎng)頁示例,需要的朋友可以參考下2014-03-03Maven profile實現(xiàn)不同環(huán)境的配置管理實踐
這篇文章主要介紹了Maven profile實現(xiàn)不同環(huán)境的配置管理實踐,本文給大家介紹的非常詳細,對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下2020-09-09原生java代碼實現(xiàn)碼云第三方驗證登錄的示例代碼
這篇文章主要介紹了原生java代碼實現(xiàn)碼云第三方驗證登錄的示例代碼,文中通過示例代碼介紹的非常詳細,對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2021-04-04java中數(shù)組list map三者之間的互轉(zhuǎn)介紹
java中 數(shù)組 list map之間的互轉(zhuǎn)一張圖清晰呈現(xiàn)并附有代碼,不懂的朋友可以參考下2013-10-10詳解java操作Redis數(shù)據(jù)庫的redis工具(RedisUtil,jedis工具JedisUtil,JedisPoo
這篇文章主要介紹了java操作Redis數(shù)據(jù)庫的redis工具,包括RedisUtil,jedis工具JedisUtil,JedisPoolUtil工具,本文通過實例代碼給大家介紹的非常詳細,需要的朋友可以參考下2021-08-08