AQS與JUC中的鎖實現原理
摘要
提到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鎖的架構
ReentrantLoc的架構相對簡單,主要包括一個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)入佇列
我們來看看,當這三個執行緒同時去競爭鎖的時候發生了什麼?
程式碼:
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
解讀:
三個執行緒同時進來,他們會首先會通過CAS去修改state的狀態,如果修改成功,那麼競爭成功,因此這個時候三個執行緒只有一個CAS成功,其他兩個執行緒失敗,也就是tryAcquire返回false。
接下來,addWaiter會把將當前執行緒關聯的EXCLUSIVE型別的節點入佇列:
程式碼:
private Node addWaiter(Node mode) {
Node node = new Node(Thread.currentThread(), mode);
Node pred = tail;
if (pred != null) {
node.prev = pred;
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
enq(node);
return node;
}
解讀:
如果隊尾節點不為null,則說明佇列中已經有執行緒在等待了,那麼直接入隊尾。對於我們舉的例子,這邊的邏輯應該是走enq,也就是開始隊尾是null,其實這個時候整個佇列都是null的。
程式碼:
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;
}
}
}
}
解讀:
如果Thread2和Thread3同時進入了enq,同時t==null,則進行CAS操作對佇列進行初始化,這個時候只有一個執行緒能夠成功,然後他們繼續進入迴圈,第二次都進入了else程式碼塊,這個時候又要進行CAS操作,將自己放在隊尾,因此這個時候又是隻有一個執行緒成功,我們假設是Thread2成功,哈哈,Thread2開心的返回了,Thread3失落的再進行下一次的迴圈,最終入佇列成功,返回自己。
2)併發問題
基於上面兩段程式碼,他們是如何實現不進行加鎖,當有多個執行緒,或者說很多很多的執行緒同時執行的時候,怎麼能保證最終他們都能夠乖乖的入佇列而不會出現併發問題的呢?這也是這部分程式碼的經典之處,多執行緒競爭,熱點、單點在佇列尾部,多個執行緒都通過【CAS+死迴圈】這個free-lock黃金搭檔來對佇列進行修改,每次能夠保證只有一個成功,如果失敗下次重試,如果是N個執行緒,那麼每個執行緒最多loop N次,最終都能夠成功。
3)掛起等待執行緒
上面只是addWaiter的實現部分,那麼節點入佇列之後會繼續發生什麼呢?那就要看看acquireQueued是怎麼實現的了,為保證文章整潔,程式碼我就不貼了,同志們自行查閱,我們還是以上面的例子來看看,Thread2和Thread3已經被放入佇列了,進入acquireQueued之後:
-
對於Thread2來說,它的prev指向HEAD,因此會首先再嘗試獲取鎖一次,如果失敗,則會將HEAD的waitStatus值為SIGNAL,下次迴圈的時候再去嘗試獲取鎖,如果還是失敗,且這個時候prev節點的waitStatus已經是SIGNAL,則這個時候執行緒會被通過LockSupport掛起。
-
對於Thread3來說,它的prev指向Thread2,因此直接看看Thread2對應的節點的waitStatus是否為SIGNAL,如果不是則將它設定為SIGNAL,再給自己一次去看看自己有沒有資格獲取鎖,如果Thread2還是擋在前面,且它的waitStatus是SIGNAL,則將自己掛起。
如果Thread1死死的握住鎖不放,那麼Thread2和Thread3現在的狀態就是掛起狀態啦,而且HEAD,以及Thread的waitStatus都是SIGNAL,儘管他們在整個過程中曾經數次去嘗試獲取鎖,但是都失敗了,失敗了不能死迴圈呀,所以就被掛起了。當前狀態如下:
鎖釋放-等待執行緒喚起
我們來看看當Thread1這個時候終於做完了事情,呼叫了unlock準備釋放鎖,這個時候發生了什麼。
程式碼:
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
解讀:
首先,Thread1會修改AQS的state狀態,加入之前是1,則變為0,注意這個時候對於非公平鎖來說是個很好的插入機會,舉個例子,如果鎖是公平鎖,這個時候來了Thread4,那麼這個鎖將會被Thread4搶去。。。
我們繼續走常規路線來分析,當Thread1修改完狀態了,判斷佇列是否為null,以及隊頭的waitStatus是否為0,如果waitStatus為0,說明佇列無等待執行緒,按照我們的例子來說,隊頭的waitStatus為SIGNAL=-1,因此這個時候要通知佇列的等待執行緒,可以來拿鎖啦,這也是unparkSuccessor做的事情,unparkSuccessor主要做三件事情:
-
將隊頭的waitStatus設定為0.
-
通過從佇列尾部向佇列頭部移動,找到最後一個waitStatus<=0的那個節點,也就是離隊頭最近的沒有被cancelled的那個節點,隊頭這個時候指向這個節點。
-
將這個節點喚醒,其實這個時候Thread1已經出佇列了。
還記得執行緒在哪裡掛起的麼,上面說過了,在acquireQueued裡面,我沒有貼程式碼,自己去看哦。這裡我們也大概能理解AQS的這個佇列為什麼叫FIFO佇列了,因此每次喚醒僅僅喚醒隊頭等待執行緒,讓隊頭等待執行緒先出。
羊群效應
這裡說一下羊群效應,當有多個執行緒去競爭同一個鎖的時候,假設鎖被某個執行緒佔用,那麼如果有成千上萬個執行緒在等待鎖,有一種做法是同時喚醒這成千上萬個執行緒去去競爭鎖,這個時候就發生了羊群效應,海量的競爭必然造成資源的劇增和浪費,因此終究只能有一個執行緒競爭成功,其他執行緒還是要老老實實的回去等待。AQS的FIFO的等待佇列給解決在鎖競爭方面的羊群效應問題提供了一個思路:保持一個FIFO佇列,佇列每個節點只關心其前一個節點的狀態,執行緒喚醒也只喚醒隊頭等待執行緒。其實這個思路已經被應用到了分散式鎖的實踐中,見:Zookeeper分散式鎖的改進實現方案。
總結
這篇文章粗略的介紹一下ReentrantLock以及鎖實現基礎框架AQS的實現原理,大致上通過舉了個三個執行緒競爭鎖的例子,從lock、unlock過程發生了什麼這個問題,深入瞭解AQS基於狀態的標識以及FIFO等待佇列方面的工作原理,最後擴充套件介紹了一下羊群效應問題,博主才疏學淺,還請多多指教。
轉自:
https://my.oschina.net/andylucc/blog/651982
關於AQS解析:
http://www.cnblogs.com/xrq730/p/7096084.html
相關文章
- AQS實現原理AQS
- Java中的鎖原理、鎖優化、CAS、AQS詳解!Java優化AQS
- JAVA AQS 實現原理JavaAQS
- java中的鎖及實現原理Java
- AQS學習(二) AQS互斥模式與ReenterLock可重入鎖原理解析AQS模式
- Redis分散式鎖的使用與實現原理Redis分散式
- Java 中佇列同步器 AQS(AbstractQueuedSynchronizer)實現原理Java佇列AQS
- AbstractQueuedSynchronizer(AQS)抽絲剝繭深入瞭解JUC框架原理AQS框架
- 分散式鎖實現原理與最佳實踐分散式
- Java併發指南8:AQS中的公平鎖與非公平鎖,CondtionJavaAQS
- ReentrantLock基於AQS的公平鎖和非公平鎖的實現區別ReentrantLockAQS
- JUC(3)---CountDownLatch、CyclicBarrier和AQSCountDownLatchAQS
- Redis、Zookeeper實現分散式鎖——原理與實踐Redis分散式
- ☕【Java技術指南】「併發原理專題」AQS的技術體系之CLH、MCS鎖的原理及實現JavaAQS
- 分散式鎖的實現原理分散式
- ReentrantLock可重入鎖、公平鎖非公平鎖區別與實現原理ReentrantLock
- 解讀 JUC —— AQS 獨佔模式AQS模式
- 分散式鎖的實現及原理分散式
- AQS學習(一)自旋鎖原理介紹(為什麼AQS底層使用自旋鎖佇列?)AQS佇列
- AQS:JAVA經典之鎖實現演算法(一)AQSJava演算法
- Java JUC 抽象同步佇列AQS解析Java抽象佇列AQS
- 輕量級分散式鎖的設計原理分析與實現分散式
- 15.AQS的今生,構建出JUC的基礎AQS
- 【Java併發】【AQS鎖】鎖在原始碼中的應用JavaAQS原始碼
- Redis分散式鎖的原理和實現Redis分散式
- zookeeper 分散式鎖的原理及實現分散式
- Redisson實現分散式鎖---原理Redis分散式
- AQS:JAVA經典之鎖實現演算法(二)-ConditionAQSJava演算法
- canal原始碼之BooleanMutex(基於AQS中共享鎖實現)原始碼BooleanMutexAQS
- 一文帶你看懂Java中的Lock鎖底層AQS到底是如何實現的JavaAQS
- 聊聊JUC包下的底層支撐類-AbstractQueuedSynchronizer(AQS)AQS
- 堆的原理與實現
- synchronized的實現原理——鎖膨脹過程synchronized
- Java多執行緒之---用 CountDownLatch 說明 AQS 的實現原理Java執行緒CountDownLatchAQS
- synchronized實現原理及鎖優化synchronized優化
- Java中鎖的實現方式Java
- Java併發指南7:JUC的核心類AQS詳解JavaAQS
- [SentencePiece]Tokenizer的原理與實現