Java-併發-關於鎖的一切
Java-併發-關於鎖的一切
摘要
本文簡要說下Java中的各種鎖和類鎖機制,還有一些相關的如sleep/yield join等,分析其實現原理,做簡單比較。
請點選右側目錄,挑選感興趣的章節觀看。
注意:最近發現本文所講偏向鎖和輕量級鎖的程式碼分析章節有誤,請大家移駕參閱死磕Synchronized底層實現–概論系列文章,檢視原始碼分析。待後續有時間我會改正本文內容。
0x01 Thread相關方法
Thread 相關方法,是鎖和類鎖程式碼中大量使用的一些基本方法。第一張簡單提一下。
1.1 sleep
1.1.1 基本概念
sleep
方法如其名,就是讓執行緒休息下,直到指定時間耗盡。- 最大的特點就是阻塞過程中,不釋放執行緒擁有的物件鎖(ObjectMonitor)。
- sleep過程,會讓出CPU時間片給其他執行緒執行。
- 底層使用linux系統的
pthread_cond_timedwait
方法實現。 - sleep方法可被中斷
1.1.2 實現原理
請點選這裡
1.1.3 Sleep對比Wait
- wait會釋放ObjectMonitor控制權;sleep不會
- wait邏輯複雜,需要首先呼叫synchronized獲取ObjectMonitor控制權,才能呼叫wait,且wait後還有放入WaitSet邏輯,喚醒時還有一系列複雜操作;而sleep實現簡單,不需要別的執行緒喚醒
- wait與sleep都能被中斷(除了sleep(0),當然對他中斷沒有意義)
1.2 yield
1.2.1 基本概念
- 該方法是給排程器一個提示,當前執行緒願意放棄佔有的CPU使用權。但注意,排程器可以忽略該提示。
- yield只是一個探索式的嘗試,期望改善多執行緒場景下某些執行緒過度使用CPU的情況。該方法的使用應經過長期效能測試,以確保它實際上具有所需的效果。
- 使用者編碼中很少能正確使用該方法。因為可能在除錯或測試的時候能達到預期,但在生產環境高併發環境下有可能導致bug!
- 該方法在jdk的如JUC併發包內被用設計來做併發控制
1.2.2 實現原理
請點選這裡
1.3 join
1.3.1 基本概念
join方法主要用來等待其他執行緒執行結束,再繼續執行自己的執行緒程式碼。
1.3.2 實現原理
請點選這裡
0x02 LockSupport
2.1 基本概念
- LockSupport是用來建立鎖和其他同步類的基本執行緒阻塞原語。
- LockSupport中有一個許可的概念
- 當呼叫
park()
方法時,如果擁有許可就立刻返回;否則也許會阻塞 - 呼叫
unpark()
會使得本來不可用的許可變為可用狀態,解除執行緒阻塞 parkNanos
可指定超時時長、parkUntil
可指定截止時間戳
- 當呼叫
- 和
java.util.concurrent.Semaphore
中許可的概念不同,LockSupport的許可每個執行緒最多能擁有1個 - LockSupport的執行緒
park
可因中斷、timeout或unpark甚至是毫無理由的返回,所以一般是通過迴圈檢查附加條件是否滿足 - LockSupport的park行為可被中斷,但不會丟擲
InterruptedException
。此時可通過interrupted(會清除中斷標記位)或isInterrupted方法判斷是否發生中斷 - LockSupport是通過呼叫Unsafe函式中的
UNSAFE.park
和UNSAFE.unpark
實現阻塞和解除阻塞的。
2.2 實現原理
請查閱原始碼解讀:Java-併發-鎖-LockSupport
2.3 LockSupport和wait/notify區別
- LockSupport中的阻塞和喚醒操作是直接作用於Thread物件的,更符合我們隊執行緒阻塞這個語義的理解,使用起來也更方便;
- 而wait/notify的呼叫是面向
Object
的,執行緒的阻塞/喚醒對Thread本身來說是被動的。而且notify是隨機喚醒的,無法精確地控制喚醒的執行緒以及喚醒的時機。程式碼上來說也很麻煩,稍不注意就會寫錯。
2.4 例子
2.4.1 普通使用例子
import java.util.concurrent.locks.LockSupport;
public class LockParkDemo1 {
private static Thread mainThread;
public static void main(String[] args) {
InnerThread it = new LockParkDemo1().new InnerThread();
Thread td = new Thread(it);
mainThread = Thread.currentThread();
System.out.println(Thread.currentThread().getName() + " start it");
td.start();
System.out.println(Thread.currentThread().getName() + " block");
// LockSupport.park(Thread.currentThread());
LockSupport.park();
System.out.println(Thread.currentThread().getName() + " continue");
}
class InnerThread implements Runnable{
@Override
public void run() {
int count = 5;
while(count>0){
System.out.println("count=" + count);
count--;
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName()+" wakup others");
LockSupport.unpark(mainThread);
}
}
}
程式輸出結果如下:
main start it
main block
Thread-0 wakup others
main continue
2.4.2 Blocker及除錯例子
- 程式碼很簡單,如下:
import java.util.concurrent.locks.LockSupport;
/**
* Created by chengc on 2018/12/15.
*/
public class BlockerTest
{
public static void main(String[] args)
{
Thread.currentThread().setName("Messi");
LockSupport.park("YangGuang");
}
}
jps
檢視該程序pid:
$ jps
73900 BlockerTest
jstack
:
$ jstack -l 73900
"Messi" #1 prio=5 os_prio=31 tid=0x00007fe34c822000 nid=0x1b03 waiting on condition [0x0000700006470000]
java.lang.Thread.State: WAITING (parking)
at sun.misc.Unsafe.park(Native Method)
- parking to wait for <0x000000076ac8fcc0> (a java.lang.String)
at java.util.concurrent.locks.LockSupport.park(LockSupport.java:175)
at demos.concurrent.lock.park.BlockerTest.main(BlockerTest.java:13)
Locked ownable synchronizers:
- None
可以看到我們的主執行緒Messi
處於WAITING
狀態,而且原因是parking
。Blocker
物件時個java.lang.String
。
2.5 應用
AQS(AbstractQueuedSynchronizer)
就是利用了LockSupport的相關方法來控制執行緒阻塞或者喚醒。
public final void awaitUninterruptibly() {
Node node = addConditionWaiter();
int savedState = fullyRelease(node);
boolean interrupted = false;
while (!isOnSyncQueue(node)) {
LockSupport.park(this);
if (Thread.interrupted())
interrupted = true;
}
if (acquireQueued(node, savedState) || interrupted)
selfInterrupt();
}
0x03 synchronized
3.1 基本概念
- 每次只能有一個執行緒進入臨界區
- 保證臨界區內共享變數的可見性和有序性
- 成功進入
synchronized
區域的執行緒可以拿到物件的Object-Monitor。具體有3種用法,作用域不同,在後面例子中介紹。 - 對於拿到鎖的執行緒來說,同一個物件的
synchronized
具有可重入性 - 不要將可變物件作為synchronized
- 如果相互等待對方的synchronized 物件,可能出現死鎖
- synchronized鎖是非公平的
3.2 實現原理
關於synchronized的實現原理可以檢視這篇文章: Java-併發-鎖-synchronized
3.3 ReentrantLock對比synchronized
ReentrantLock和synchronized對比如下:
可重入 | 等待可中斷 | 公平性 | 繫結物件數 | 效能優化 | |
---|---|---|---|---|---|
synchronized | 支援 | 不支援 | 非公平 | 只能1個 | 較多 |
ReentrantLock | 支援 | 支援 | 非公平/公平 | 可以多個 | - |
0x04 鎖優化
4.1 基本概念
4.1.1 什麼是鎖優化
從JDK 1.6
開始HotSpot虛擬機器團隊花了很多精力實現各種鎖的優化技術,主要目的很明顯就是為了更少的阻塞、更少的競爭、更高效的獲取鎖和釋放鎖,說白了就是提高多執行緒需要訪問共享區間的執行效率。
4.1.2 鎖與物件
JavaHeap
中的物件主要包括三部分:
4.1.2.1 例項資料。
這部分是物件真正儲存的有效資訊,各種型別欄位內容,還包括父類繼承過來的資訊。
4.1.2.2 位元組對齊填充
只是佔位符,因為HotSpot記憶體管理要求物件起始地址必須是8位元組整數倍,即物件長度必是8位元組整數倍。而物件頭一般來說已經是整數倍,所以位元組填充主要是為例項資料填充。
4.1.2.3 物件頭
Mark Word
,即物件執行時資料。他的內部位元組長度分佈與含義非固定,節約空間。存有如hashCode、分代年齡、鎖標誌資訊等。Klass Pointer
,即型別指標(用於確定物件屬於的類),指向方法區中的該物件Class型別物件。- 如果物件是陣列,還有個陣列長度。
下面是一個32位的HotSpot虛擬機器中 MarkWord示意圖:
Mark Word
中的最後2bit就是鎖狀態的標誌位,用來記錄當前物件的鎖狀態:
狀態 | 標誌位 | 儲存內容 |
---|---|---|
未鎖定 | 01 | 物件雜湊碼、物件分代年齡 |
輕量級鎖定 | 00 | 指向鎖記錄的指標 |
膨脹(重量級鎖) | 10 | 執行重量級鎖定的指標 |
GC標記 | 11 | 空,不需要記錄資訊 |
可偏向 | 01 | 偏向執行緒ID、偏向時間戳、物件分代年齡 |
注意上圖中的後方1bit還會在無鎖和偏向鎖時不同以區分兩種鎖狀態,因為他們的最後2bit鎖標誌位都是01。
jdk8/hotspot/src/share/vm/oops/markOop.hpp
描述了物件頭部資訊,有興趣的讀者可以看看。
4.2 自旋鎖-SpinLock
4.2.1 思想
使用互斥鎖的時候,往往阻塞時間其實很短,但執行緒阻塞和喚醒操作由使用者態轉為核心態,效能開銷大。
這個時候自旋鎖產生了,他的思想很樸素,前提是有多於1個CPU:
- 執行緒一請求並獲取鎖
- 執行緒二請求鎖,發現執行緒一持有鎖
- 執行緒二並不放棄CPU進入等待,而是進入空迴圈即自旋,看是否執行緒一很快就釋放鎖
4.2.2 小結
- 優點
在總是能較短時間獲取鎖、執行緒競爭不激烈時,可僅自旋而不是執行緒阻塞和喚醒,對效能提升大。 - 缺點
自旋鎖的問題顯而易見,就是等待鎖的時候佔有CPU資源空跑。可以使用-XX:PreBlockSpin
修改自旋次數,預設為10次。
4.3 自適應自旋
這個自適應自旋鎖思想也很樸素,相當於基於HBO(歷史)的優化:
- 如果前一次獲取鎖很快,那本次就允許自旋次數多一些如100,因為JVM認為這一次也能成功獲取鎖
- 如果鎖很難獲取,自旋鎖很少成功,那甚至可以直接不自旋,直接阻塞執行緒進行等待
4.4 鎖消除
鎖消除,顧名思義,就是JVM在編譯器執行時會掃描程式碼,當檢查到那些不可能存在共享區競爭但卻有互斥同步的程式碼,直接將這樣的多此一舉的鎖消除。
除了那些經驗不足的程式設計人員會寫無意義的同步程式碼,還有很多是JVM幫程式加上的,比如以下程式碼:
public String connectStrs(String str1, String str2, String str3){
return str1 + str2 + str3;
}
會因為String
是不可變類,反覆產生新物件,所以被JVM自動優化成以下形式(JDK1.5之前版本,1.5之後是StringBuilder了):
public String connectStrs(String str1, String str2, String str3){
StringBuffer sb = new StringBuffer;
sb.append(str1);
sb.append(str2);
sb.append(str3);
}
此時,StringBuffer
是帶鎖的了。
鎖消除的主要依據是逃逸分析,詳見。這裡簡單說下,就是指程式碼中的位於JavaHeap的所有資料都不會逃逸導致被其他執行緒訪問,那就將可將他們作為棧內資料,作為執行緒私有。這樣一來同步鎖就沒有意義了,可以消除。
4.5 鎖粗化
這個名字有點詭異,其實說白了就是擴大鎖的範圍。
什麼?不是說好了要儘量減小同步鎖的適用範圍,縮短佔有鎖的時間嗎?!
其實,JVM是會在反覆在段程式碼中對同一物件加鎖的情況進行鎖粗化優化的。
比如
public String optimizedConnectStrs(String str1, String str2, String str3){
StringBuffer sb = new StringBuffer;
sb.append(str1);
sb.append(str2);
sb.append(str3);
}
這種情況每個append
都會執行如下程式碼:
public synchronized StringBuffer append(String str) {
toStringCache = null;
super.append(str);
return this;
}
也就是說會反覆對sb
這個物件監視器加synchronized同步鎖。
此時,JVM就會進行優化,將鎖包住多次append操作的起始,只需加鎖一次。這就是所謂鎖粗化。
4.6 鎖升級
4.6.1 基本概念
- 前面已經提到過Java中4種鎖狀態,隨著鎖競爭開始,這幾種鎖之間有鎖升級的關係:
無鎖 -> 偏向鎖 -> 輕量級鎖 -> 重量級鎖
- 鎖只能升級不能降級。這麼做的原因是縮短鎖的獲取釋放週期,提升效率。
4.6.2 重量級鎖
4.6.2.1 基本概念
重量級鎖就是前面提到過的傳統的基於ObjectMonitor
的鎖synchronized
,底層使用MutexLock
。使用這類互斥鎖的時候,往往阻塞時間其實很短,但執行緒阻塞和喚醒操作會有使用者態和核心態轉換,效能開銷大。
4.6.2.2 MutexLock對比SpinLock
上述的monitorLock底層採用MutexLock實現,他和自旋鎖SpinLock對比如下:
MutexLock | SpinLock | |
---|---|---|
原理 | 嘗試獲取鎖,若可得到就佔有;若不能,就阻塞等待 | 嘗試獲取鎖,若可得到就佔有。若不能,空轉並持續嘗試直到獲取 |
使用場景 | 當執行緒進入阻塞沒有很大問題,或需要等待一段足夠長的時間才能獲取鎖 | 當執行緒不應該進入睡眠如中斷處理等或只需等待非常短的時間就能獲取鎖 |
缺點 | 引起執行緒切換和執行緒排程開銷大 | 執行緒空跑CPU等待,浪費資源 |
4.6.3 輕量級鎖
4.6.3.1 思想
JDK1.6後引入
。
該輕量級鎖的名字是相對於傳統的那些鎖來說,認為傳統同步鎖(重量級鎖)開銷極大,大部分鎖其實在同步期間並沒有競爭,沒必要使用重量級鎖導致不必要開銷。
輕量級鎖加鎖過程圖:
- 程式碼進入同步塊時,如果該同步物件未被鎖定(01)且偏向鎖標誌位為0,JVM就會在當前執行緒的棧中建立一個
Lock Record
空間,他是一個鎖物件頭的Mark Word
的內容拷貝,名為Displaced Mark Word
。 - JVM以CAS(鎖物件, MarkWord, DisplacedMarkWord) 即把MarkWord更新為指向複製的
Lock Record
的指標 - 如果第2步成功,就認為該執行緒擁有這個物件鎖。此時將
MarkWord
最後兩bit
標記為 00,表示輕量級鎖狀態。 - 如果第2步失敗,JVM就檢查鎖物件的
MarkWord
是否指向當前執行緒的棧幀。如果是,就說明當前執行緒擁有了該物件鎖,這是鎖重入,可以開始執行同步塊內程式碼;否則說明被其他執行緒擁有鎖就膨脹為重量級鎖,此時會標記為10。在膨脹過程中,其他執行緒全部阻塞等待,而當前執行緒會使用4.1章節中提到的自旋鎖等待膨脹完成,避免阻塞。待膨脹為重量級鎖完成後重新競爭同步鎖。
輕量級鎖膨脹為重量級鎖的過程可以在jdk8/hotspot/src/share/vm/runtime/synchronizer.cpp
的ObjectMonitor * ATTR ObjectSynchronizer::inflate (Thread * Self, oop object)
方法程式碼中看到,這裡不再展開。
解鎖過程如下:
- CAS(鎖物件, DisplacedMarkWord, MarkWord)
- 如果第1步成功,同步結束
- 第1步失敗,說明其他執行緒嘗試過獲取該鎖。此時不僅要釋放鎖,同時需要喚醒被掛起等待的執行緒,鎖也要膨脹為重量級鎖。
4.6.3.2 小結
輕量級鎖的依據是大部分鎖在同步期間沒有競爭,從而用CAS方式避免了使用互斥量開銷。
但如果執行緒競爭鎖激烈的場景,就會額外加上CAS的開銷。此時反而效率低於所謂的重量級鎖了。
4.6.4 偏向鎖
4.6.4.1 思想
JDK1.6後引入
。
相對於輕量級鎖是消除無競爭時用CAS
消除同步原語,偏向鎖是直接在無競爭時消除所有同步。
當開啟了偏向鎖配置(-XX:+UserBiasedLocking
)時,偏向鎖加鎖過程如下:
- 當鎖物件第一次被某個執行緒獲取,JVM就將物件頭中的鎖標誌位設為01,即可偏向模式,同時CAS方式更新偏向鎖內容,如將執行緒ID指向當前執行緒等資訊。
- 如果上一步CAS成功,那麼持有該偏向鎖的執行緒以後每次獲取該鎖進入同步塊時,檢查
Mark Word
中執行緒ID是否是當前執行緒ID。如果是,那麼可以直接執行同步塊程式碼,JVM可以不用再進行加解鎖、更新偏向資訊等同步操作,效率提高很多 - 當有別的執行緒開始獲取該鎖時,可偏向模式結束,進入安全點(
SafePoint
)。此時需撤銷偏向鎖,會導致stop the word
暫停擁有偏向鎖執行緒,判斷是否處於被鎖定狀態:- 如果此時已鎖定,就重設為輕量級鎖(00)。
- 如果無鎖,就設為未鎖定狀態(01)。
偏向鎖的釋放:
偏向鎖釋放鎖的動作是被動的,如加鎖過程中第三步即在其他執行緒嘗試獲取競爭偏向鎖時才會觸發偏向鎖釋放過程。上面說的安全點指在該時間點上沒有程式碼執行。
4.6.4.2 小結
- 優點
線上程競爭少時,偏向鎖使得執行緒僅需在獲取鎖進入同步塊時有JVM一些相關同步操作,後面每次該執行緒進入同步塊都不再需要額外操作(比輕量級鎖更輕,不需每次做CAS了),對執行緒效能提高十分有利。 - 缺點
但對於競爭激烈的場景中,偏向鎖反而低效,此時可以考慮禁用偏向鎖。
4.6.5 鎖升級小結
初始分配物件時分為開啟/不開啟偏向鎖模式。注意,初始時,偏向鎖模式開啟,但是擁有鎖執行緒ID為0,代表未鎖定。
4.7 鎖優化小結
在學習了前面幾種類別的鎖後,再把synchronized加鎖過程串起來講一下,前提已經開啟偏向鎖:
- 第一次進入的執行緒獲取偏向鎖,將
ownerId
設為自己 - 後序進入的該執行緒都會檢查該鎖物件ownerID,如果是自己就直接利用偏向鎖執行同步塊
- 後續進入的其他執行緒檢查到鎖物件ownerID不是自己,偏向鎖模式結束,升級為輕量級鎖。複製一份
Mark Word
為Displaced Mark Word
,且CAS(鎖物件, MarkWord, DisplacedMarkWord) - 以後每次進入時,CAS前先檢查
0x05 wait notify
wait
notify
還有個notifyAll
都是執行緒通訊的常用手段。
有一個先導概念就是物件鎖和類鎖,他們其實都是物件監視器Object Monitor
,只不過類鎖是類物件的監視器,可以看另一篇文章:
Java-併發-鎖-synchronized之物件鎖和類鎖
5.1 基本概念
5.1.1 wait
- 作用
顧名思義,wait其實就是執行緒用來做阻塞等待的。 - 超時引數
在JDK的Object中,wait方法分為帶引數和無引數版本,這裡說的引數就是等待超時的引數。 - 中斷
其他執行緒在當前執行緒執行wait之前或正在wait時,對當前執行緒呼叫中斷interrupted
方法,會丟擲InterruptedException
,且中斷標記會被自動清理。
5.1.2 notify
- 該方法用來任意喚醒一個在物件鎖的等待集的執行緒(其實看了原始碼會發現不是任意的,而是一個WaitQueue,FIFO)。
- 但要注意,被喚醒的執行緒不會馬上開始執行,因為物件鎖還被呼叫
notify
的執行緒擁有,直到退出synchronized
塊。 - 喚醒後的執行緒跟其他執行緒一起競爭該同步物件鎖。
- 注意,該方法和wait方法一樣也必須是擁有該物件同步物件鎖的執行緒才能呼叫,否則丟擲
IllegalMonitorStateException
。
5.1.3 notifyAll
- 該方法用來喚醒所有在物件鎖的等待集的執行緒。
- 但要注意,被喚醒的執行緒不會馬上開始執行,因為物件鎖還被呼叫
notifyAll
的執行緒擁有。 - 喚醒後的執行緒跟其他執行緒一起競爭該同步物件鎖。
- 注意,該方法和wait方法一樣也必須是擁有該物件同步物件鎖的執行緒才能呼叫,否則丟擲
IllegalMonitorStateException
。
5.2 實現原理
5.3 wait與sleep比較
經常面試會問這個問題,往往我們都是網上查資料死記硬背。現在我們都看完了原始碼(sleep原始碼點這裡),可以得出以下結論
- wait會釋放ObjectMonitor控制權;sleep不會
- wait邏輯複雜,需要首先呼叫synchronized獲取ObjectMonitor控制權,才能呼叫wait,且wait後還有放入WaitSet邏輯,喚醒時還有一系列複雜操作;而sleep實現簡單,不需要別的執行緒喚醒
- wait與sleep都能被中斷(除了sleep(0),當然對他中斷沒有意義)
5.4 Condition.await/signal對比wait/notify
5.4.1 Condition和Object關係
等待 | 喚醒 | 喚醒全部 | |
---|---|---|---|
Object | wait | notify | notifyAll |
Condition | await | signal | signalAll |
5.4.2 wait和await對比
中斷 | 超時精確 | Deadline | |
---|---|---|---|
wait | 可中斷 | 可為納秒 | 不支援 |
await | 支援可中斷/不可中斷 | 可為納秒 | 支援 |
5.4.3 notify和signal對比
全部喚醒 | 喚醒順序 | 執行前提 | 邏輯 | |
---|---|---|---|---|
notify | 支援,notifyAll | 隨機(jdk寫的,其實cpp原始碼是一個wait_queue,FIFO) | 擁有鎖 | 從wait_list取出,放入entry_list,重新競爭鎖 |
signal | 支援,signalAll | 順序喚醒 | 擁有鎖 | 從condition_queue取出,放入wait_queue,重新競爭鎖 |
5.4.4 底層原理對比
- Object的阻塞和喚醒,前基於synchronized的。底層實現是在cpp級別,呼叫synchronized的執行緒物件會放入entry_list,競爭到鎖的執行緒處於
active
狀態。呼叫wait方法後,執行緒物件被放入wait_queue。而notify會按FIFO方法從wait_queue中取得一個物件並放回entry_list,這樣該執行緒可以重新競爭synchronized同步鎖了。 - Condition的阻塞喚醒,是基於lock的。lock維護了一個wait_queue,用於存放等待鎖的執行緒。而Condition也維護了一個condition_queue。當擁有鎖的執行緒呼叫await方法,就會被放入condition_queue;當呼叫signal方法,會從condition_queue選頭一個滿足要求的節點移除然後放入wait_queue,重新競爭lock。
5.4.5 應用場景對比
- Object使用比較單一,只能針對一個條件。
- 一個ReentrantLock可以有多個Condition,對應不同條件。比如在生產者消費者可以這樣實現:
private static ReentrantLock lock = new ReentrantLock();
private static Condition notEmpty = lock.newCondition();
private static Condition notFull = lock.newCondition();
// 生產者
public void produce(E item) {
lock.lock();
try {
while(isFull()) {
// 資料滿了,生產者就阻塞,等待消費者消費完後喚醒
notFull.await();
}
// ...生產資料程式碼
// 喚醒消費者執行緒,告知有資料了,可以消費
notEmpty.signal();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
// 消費者
public E consume() {
lock.lock();
try {
while(isEmpty()) {
// 資料空了,消費者就阻塞,等待生產者生產資料後喚醒
notEmpty.await();
}
// ...消費資料程式碼
// 喚醒生產者者執行緒,告知有資料了,可以消費
notFull.signal();
return item;
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
return null;
}
這樣好處就很明顯了。如果使用Object,那麼喚醒的時候也許就喚醒了同類的角色執行緒。而使用condition可以在只有一個鎖的情況下,實現我們想要的只喚醒對方角色執行緒的功能。
0x06 CAS
6.1 基本概念
JDK中大量程式碼使用了CAS,底層是呼叫的sun.misc.Unsafe
,如Unsafe.compareAndSwapInt
方法:
public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);
該方法第一個引數為物件,第二個引數為指定field
在物件中的偏移量,第三個為期望值,最後一個是要更新的目標值。
CAS的基本思想就是原子性的執行以下兩個操作:
- 比較物件中的field當前值是否為期望值
- 如果是就更新為指定值,否則不更新
那麼,java是怎麼實現這個操作的原子性的呢?我們接著往下看
6.2 實現原理
透過前面的程式碼,可以看到compareAndSwapInt
是一個JNI
呼叫。
在jdk8/hotspot/src/share/vm/prims/unsafe.cpp
中可以找到以下內容:
{CC"compareAndSwapInt", CC"("OBJ"J""I""I"")Z", FN_PTR(Unsafe_CompareAndSwapInt)},
UNSAFE_ENTRY(jboolean, Unsafe_CompareAndSwapInt(JNIEnv *env, jobject unsafe, jobject obj, jlong offset, jint e, jint x))
UnsafeWrapper("Unsafe_CompareAndSwapInt");
oop p = JNIHandles::resolve(obj);
// 獲取該filed記憶體地址
jint* addr = (jint *) index_oop_from_field_offset_long(p, offset);
// 呼叫Atomic.cmpxchg方法
return (jint)(Atomic::cmpxchg(x, addr, e)) == e;
UNSAFE_END
鑑於本人能力有限,就不再繼續向下了。有興趣的讀者可以研究下jdk8/hotspot/src/share/vm/runtime/atomic.cpp
也可參考文章:
0x07 ReentrantLock
7.1 基本概念
ReentrantLock
是使用最廣的、最出名的AQS(AbstractQueuedSynchronizer)
系列的可重入鎖。
它屬於是高層API。和synchronized對比如下:
可重入性 | 等待可中斷 | 公平性 | 繫結物件數 | 效能優化 | |
---|---|---|---|---|---|
synchronized | 支援 | 不支援 | 非公平 | 只能1個 | 較多 |
ReentrantLock | 支援 | 支援 | 非公平/公平 | 可以多個 | - |
- 等待可中斷
獲取鎖時可以指定一個超時時間,如果超過這個時間還沒有拿到鎖就放棄等待 - 公平性
公平鎖就是按執行緒申請鎖時候FIFO的方式獲取鎖;而非公平鎖沒有這個規則,所有執行緒共同競爭,沒有先來後到一說 - 繫結物件
一個synchronized
繫結一個Object用來wait
,notify
等操作;而ReentrantLock可以newCondition多次等到多個Condition例項,執行await
,signal
等方法。
7.2 實現原理
限於篇幅,這裡可以大概說下其原理。
7.2.1 AQS
AQS全稱AbstractQueuedSynchronizer
,他是ReentrantLock
內部類NonfairSync
和FairSync
的父類Sync
的父類,其核心元件如下:
- state,int 型別,用來儲存許可數
- Node雙向連結串列,儲存等待鎖的執行緒
該Node
就是AQS的內部類,這裡可以簡單看看Node定義:
static final class Node {
// 表明等待的節點處於共享鎖模式,如Semaphore:addWaiter(Node.SHARED)
static final Node SHARED = new Node();
// 表明等待的節點處於排他鎖模式,如ReentranLock:addWaiter(Node.EXCLUSIVE)
static final Node EXCLUSIVE = null;
// 執行緒已撤銷狀態
static final int CANCELLED = 1;
// 後繼節點需要unpark
static final int SIGNAL = -1;
// 執行緒wait在condition上
static final int CONDITION = -2;
// 使用在共享模式頭Node有可能處於這種狀態, 表示鎖的下一次獲取可以無條件傳播
static final int PROPAGATE = -3;
// 這個waitStatus就是存放以上int狀態的變數,預設為0
// 用volatile修飾保證多執行緒時的可見性和順序性
volatile int waitStatus;
// 指向前一個Node的指標
volatile Node prev;
// 指向後一個Node的指標
volatile Node next;
// 指向等待的執行緒
volatile Thread thread;
// condition_queue中使用,指向下一個conditionNode的指標
Node nextWaiter;
// 判斷是否共享鎖模式
final boolean isShared() {
return nextWaiter == SHARED;
}
// 返回前驅結點,當前驅結點為null時丟擲NullPointerException
final Node predecessor() throws NullPointerException {
Node p = prev;
if (p == null)
throw new NullPointerException();
else
return p;
}
// 用來初始化wait佇列的構造方法;也被用來做共享鎖模式
Node() {