reentrantlock非公平锁不会随机挂起线程?_程序员必须要知道的ReentrantLock 及 AQS 实现原理...
專注于Java領域優質技術,歡迎關注
作者:Float_Luuu
提到 JAVA 加鎖,我們通常會想到 synchronized 關鍵字或者是 Java Concurrent Util(后面簡稱JCU)包下面的 Lock,今天就來扒一扒 Lock 是如何實現的,比如我們可以先提出一些問題:當我們通實例化一個 ReentrantLock 并且調用它的 lock 或 unlock 的時候,這其中發生了什么?如果多個線程同時對同一個鎖實例進行 lock 或 unlcok 操作,這其中又發生了什么?
什么是可重入鎖?
ReentrantLock 是可重入鎖,什么是可重入鎖呢?可重入鎖就是當前持有該鎖的線程能夠多次獲取該鎖,無需等待。可重入鎖是如何實現的呢?這要從 ReentrantLock 的一個內部類 Sync 的父類說起,Sync 的父類是 AbstractQueuedSynchronizer(后面簡稱AQS)。
什么是AQS?
AQS 是 JDK1.5 提供的一個基于 FIFO 等待隊列實現的一個用于實現同步器的基礎框架,這個基礎框架的重要性可以這么說,JCU 包里面幾乎所有的有關鎖、多線程并發以及線程同步器等重要組件的實現都是基于 AQS 這個框架。AQS 的核心思想是基于 volatile int state 這樣的一個屬性同時配合 Unsafe 工具對其原子性的操作來實現對當前鎖的狀態進行修改。當 state 的值為 0 的時候,標識改 Lock 不被任何線程所占有。
ReentrantLock 鎖的架構
ReentrantLock 的架構相對簡單,主要包括一個 Sync 的內部抽象類以及 Sync 抽象類的兩個實現類。上面已經說過了 Sync 繼承自 AQS,他們的結構示意圖如下:
上圖除了 AQS 之外,我把 AQS 的父類 AbstractOwnableSynchronizer(后面簡稱AOS)也畫了進來,可以稍微提一下,AOS 主要提供一個 exclusiveOwnerThread 屬性,用于關聯當前持有該所的線程。另外、Sync 的兩個實現類分別是 NonfairSync 和 FairSync,由名字大概可以猜到,一個是用于實現公平鎖、一個是用于實現非公平鎖。那么 Sync 為什么要被設計成內部類呢?我們可以看看 AQS 主要提供了哪些 protect 的方法用于修改 state 的狀態,我們發現 Sync 被設計成為安全的外部不可訪問的內部類。ReentrantLock 中所有涉及對 AQS 的訪問都要經過 Sync,其實,Sync 被設計成為內部類主要是為了安全性考慮,這也是作者在 AQS 的 comments 上強調的一點。
AQS 的等待隊列
作為 AQS 的核心實現的一部分,舉個例子來描述一下這個隊列長什么樣子,我們假設目前有三個線程 Thread1、Thread2、Thread3 同時去競爭鎖,如果結果是 Thread1 獲取了鎖,Thread2 和 Thread3 進入了等待隊列,那么他們的樣子如下:
AQS 的等待隊列基于一個雙向鏈表實現的,HEAD 節點不關聯線程,后面兩個節點分別關聯 Thread2 和 Thread3,他們將會按照先后順序被串聯在這個隊列上。這個時候如果后面再有線程進來的話將會被當做隊列的 TAIL。
1)入隊列
我們來看看,當這三個線程同時去競爭鎖的時候發生了什么?代碼:
解讀:三個線程同時進來,他們會首先會通過 CAS 去修改 state 的狀態,如果修改成功,那么競爭成功,因此這個時候三個線程只有一個 CAS 成功,其他兩個線程失敗,也就是 tryAcquire 返回 false。接下來,addWaiter 會把將當前線程關聯的 EXCLUSIVE 類型的節點入隊列:
解讀:如果隊尾節點不為 null,則說明隊列中已經有線程在等待了,那么直接入隊尾。對于我們舉的例子,這邊的邏輯應該是走 enq,也就是開始隊尾是 null,其實這個時候整個隊列都是 null 的。代碼:
解讀:如果 Thread2 和 Thread3 同時進入了 enq,同時 t==null,則進行 CAS 操作對隊列進行初始化,這個時候只有一個線程能夠成功,然后他們繼續進入循環,第二次都進入了 else 代碼塊,這個時候又要進行 CAS 操作,將自己放在隊尾,因此這個時候又是只有一個線程成功,我們假設是 Thread2 成功,哈哈,Thread2 開心的返回了,Thread3 失落的再進行下一次的循環,最終入隊列成功,返回自己。
2)并發問題
基于上面兩段代碼,他們是如何實現不進行加鎖,當有多個線程,或者說很多很多的線程同時執行的時候,怎么能保證最終他們都能夠乖乖的入隊列而不會出現并發問題的呢?這也是這部分代碼的經典之處,多線程競爭,熱點、單點在隊列尾部,多個線程都通過【CAS+死循環】這個free-lock黃金搭檔來對隊列進行修改,每次能夠保證只有一個成功,如果失敗下次重試,如果是N個線程,那么每個線程最多 loop N 次,最終都能夠成功。
3)掛起等待線程
上面只是 addWaiter 的實現部分,那么節點入隊列之后會繼續發生什么呢?那就要看看 acquireQueued 是怎么實現的了,為保證文章整潔,代碼我就不貼了,同志們自行查閱,我們還是以上面的例子來看看,Thread2 和 Thread3 已經被放入隊列了,進入 acquireQueued 之后:
如果 Thread1 死死的握住鎖不放,那么 Thread2 和 Thread3 現在的狀態就是掛起狀態啦,而且 HEAD,以及 Thread 的 waitStatus 都是 SIGNAL,盡管他們在整個過程中曾經數次去嘗試獲取鎖,但是都失敗了,失敗了不能死循環呀,所以就被掛起了。當前狀態如下:
鎖釋放-等待線程喚起
我們來看看當 Thread1 這個時候終于做完了事情,調用了 unlock 準備釋放鎖,這個時候發生了什么。代碼:
解讀:首先,Thread1 會修改AQS的state狀態,加入之前是 1,則變為 0,注意這個時候對于非公平鎖來說是個很好的插入機會,舉個例子,如果鎖是公平鎖,這個時候來了 Thread4,那么這個鎖將會被 Thread4 搶去。。。
我們繼續走常規路線來分析,當 Thread1 修改完狀態了,判斷隊列是否為 null,以及隊頭的 waitStatus 是否為 0,如果 waitStatus 為 0,說明隊列無等待線程,按照我們的例子來說,隊頭的 waitStatus 為 SIGNAL=-1,因此這個時候要通知隊列的等待線程,可以來拿鎖啦,這也是 unparkSuccessor 做的事情,unparkSuccessor 主要做三件事情:
還記得線程在哪里掛起的么,上面說過了,在 acquireQueued 里面,我沒有貼代碼,自己去看哦。這里我們也大概能理解 AQS 的這個隊列為什么叫 FIFO 隊列了,因此每次喚醒僅僅喚醒隊頭等待線程,讓隊頭等待線程先出。
羊群效應
這里說一下羊群效應,當有多個線程去競爭同一個鎖的時候,假設鎖被某個線程占用,那么如果有成千上萬個線程在等待鎖,有一種做法是同時喚醒這成千上萬個線程去去競爭鎖,這個時候就發生了羊群效應,海量的競爭必然造成資源的劇增和浪費,因此終究只能有一個線程競爭成功,其他線程還是要老老實實的回去等待。AQS 的 FIFO 的等待隊列給解決在鎖競爭方面的羊群效應問題提供了一個思路:保持一個 FIFO 隊列,隊列每個節點只關心其前一個節點的狀態,線程喚醒也只喚醒隊頭等待線程。其實這個思路已經被應用到了分布式鎖的實踐中,見:Zookeeper 分布式鎖的改進實現方案。
總結
這篇文章粗略的介紹一下 ReentrantLock 以及鎖實現基礎框架 AQS 的實現原理,大致上通過舉了個三個線程競爭鎖的例子,從 lock、unlock 過程發生了什么這個問題,深入了解 AQS 基于狀態的標識以及 FIFO 等待隊列方面的工作原理,最后擴展介紹了一下羊群效應問題,博主才疏學淺,還請多多指教。
總結
以上是生活随笔為你收集整理的reentrantlock非公平锁不会随机挂起线程?_程序员必须要知道的ReentrantLock 及 AQS 实现原理...的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 浦发银行信用卡怎么设置交易密码
- 下一篇: 【转】Wireshark网络抓包(四)—