從零開始瞭解多執行緒 之 深入淺出AQS -- 上
阿新 • • 發佈:2020-12-14
# java鎖&AQS深入淺出學習--上
上一篇文章中我們一起學習了jvm快取一致性、多執行緒間的原子性、有序性、指令重排的相關內容,
這一篇文章便開始和大家一起學習學習AQS(AbstractQueuedSynchronizer)的內容
主要是包含以下三個方面
synchronized
ReentrantLock
AbstractQueuedSynchronizer
1.瞭解併發同步器
多執行緒程式設計中,有可能會出現多個執行緒同時訪問同一個共享、可變資源的情況;這種資源可能是:物件、變數、檔案等。
共享:資源可以由多個執行緒同時訪問
可變:資源可以在其生命週期內被修改
由此可以得出 由於執行緒執行的過程是不可控的,所以需要採用同步機制來協同對物件可變狀態的訪問
那麼我們怎麼解決執行緒併發安全問題?
實際上,所有的併發模式在解決執行緒安全問題時,採用的方案都是序列化訪問臨界資源。
即在同一時刻,只能有一個執行緒訪問臨界資源,也稱作同步互斥訪問。
Java 中,提供了兩種方式來實現同步互斥訪問:synchronized和Lock
同步器的本質就是加鎖
加鎖目的:序列化訪問臨界資源,即同一時刻只能有一個執行緒訪問臨界資源(同步互斥訪問)
不過有一點需要區別的是:
當多個執行緒執行一個方法時,該方法內部的區域性變數 並不是臨界資源,
因為這些區域性變數是在每個執行緒的私有棧中,因此不具有共享性,不會導致執行緒安全問題
其中鎖包括 顯式鎖 和 隱式鎖
![](https://img2020.cnblogs.com/blog/2197806/202012/2197806-20201213215625398-221914467.png)
![](https://img2020.cnblogs.com/blog/2197806/202012/2197806-20201213215648616-137208408.png)
顯式: ReentrantLock
ReentrantLock,實現juc裡Lock,實現是基於AQS實現,需要手動加鎖跟解鎖ReentrantLock lock(),unlock();
隱式: Synchronized
Synchronized加鎖機制,Jvm內建鎖,不需要手動加鎖與解鎖,Jvm會自動加鎖跟解鎖
synchronized原理詳解
synchronized內建鎖是一種物件鎖(鎖的是物件而非引用),作用粒度是物件,可以用來實現對臨界資源的同步互斥訪問,是可重入的
以下是他的三種加鎖方式:
加鎖的方式: 同步例項方法,鎖是當前例項物件(加入spring容器管理的,鎖是當前例項物件的時候,不能是多例的)
同步類方法,鎖是當前類物件
同步程式碼塊,鎖是括號裡面的物件
JVM內建鎖通過synchronized使用,通過內部物件Monitor(監視器鎖)實現,基於進入與退出Monitor物件實現方法與程式碼塊同步,
監視器鎖的實現依賴底層作業系統的Mutex lock(互斥鎖)實現,它是一個重量級鎖效能較低
/**
*越過jvm直接操作記憶體的工具
* @author njw
*/
public class UnsafeInstance {
public static Unsafe reflectGetUnsafe() {
try {
Field field = Unsafe.class.getDeclaredField("theUnsafe");
field.setAccessible(true);
return (Unsafe) field.get(null);
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
/**
* 不使用lock,怎麼實現跨方法進行加鎖和釋放?
* 方法:可以通過Unsafe來實現
* synchronized底層實現位元組碼翻譯之後 便是如此的
*/
private Object object = new Object();
public void test(){
reflectGetUnsafe().monitorEnter(object);
}
public void test1(){
reflectGetUnsafe().monitorExit(object);
}
}
synchronized 底層原理
synchronized是基於JVM內建鎖實現,通過內部物件Monitor(監視器鎖)實現,基於進入與退出Monitor物件實現方法與程式碼塊同步,
監視器鎖的實現依賴底層作業系統的Mutex lock(互斥鎖)實現,它是一個重量級鎖效能較低。
JVM內建鎖在1.5之後版本做了重大的優化,如鎖粗化(LockCoarsening)、鎖消除(Lock Elimination)、
輕量級鎖(LightweightLocking)、偏向鎖(Biased Locking)、適應性自旋(Adaptive Spinning)等技術來減少鎖操作的開銷,內建鎖的併發效能已經基本與Lock持平。
synchronized關鍵字被編譯成位元組碼後會被翻譯成monitorenter和monitorexit 兩條指令分別在同步塊邏輯程式碼的起始位置與結束位置
![](https://img2020.cnblogs.com/blog/2197806/202012/2197806-20201213215726135-211088675.png)
每個同步物件都有一個自己的Monitor(監視器鎖),加鎖過程如下圖所示:
![](https://img2020.cnblogs.com/blog/2197806/202012/2197806-20201213215835335-1028022140.png)
那麼有個問題來了,我們知道synchronized加鎖加在物件上,物件是如何記錄鎖狀態的呢?
答案是鎖狀態是被記錄在每個物件的物件頭(Mark Word)中,下面我們一起認識一下物件的記憶體佈局
![記憶體物件佈局圖](https://img2020.cnblogs.com/blog/2197806/202012/2197806-20201213215901475-1185385777.png)
2. 物件的記憶體佈局
HotSpot虛擬機器中,物件在記憶體中儲存的佈局可以分為三塊區域:
物件頭(Header)、例項資料(Instance Data)和對齊填充(Padding)。
物件頭:比如 hash碼,物件所屬的年代,物件鎖,鎖狀態標誌,偏向鎖(執行緒)ID,偏向時間,陣列長度(陣列物件)等
例項資料:即建立物件時,物件中成員變數,方法等
對齊填充:物件的大小必須是8位元組的整數倍
物件頭
HotSpot虛擬機器的物件頭包括兩部分資訊,第一部分是“Mark Word”,用於儲存物件自身的執行時資料,
如雜湊碼(HashCode)、GC分代年齡、鎖狀態標誌、執行緒持有的鎖、偏向執行緒ID、偏向時間戳等等,
這部分資料的長度在32位和64位的虛擬機器(暫 不考慮開啟壓縮指標的場景)中分別為32個和64個Bits,
官方稱它為“Mark Word”。物件需要儲存的執行時資料很多,其實已經超出了32、64位Bitmap結構所能記錄的限度,
但是物件頭資訊是與物件自身定義的資料無關的額 外儲存成本,考慮到虛擬機器的空間效率,
Mark Word被設計成一個非固定的資料結構以便在極小的空間記憶體儲儘量多的資訊,它會根據物件的狀態複用自己的儲存空間。
例如在32位的HotSpot虛擬機器中物件未被鎖定的狀態下,Mark Word的32個Bits空間中的25Bits用於儲存物件雜湊碼(HashCode),
4Bits用於儲存物件分代年齡,2Bits用於儲存鎖標誌位,1Bit固定為0,
物件頭資訊是與物件自身定義的資料無關的額外儲存成本,但是考慮到虛擬機器的空間效率,Mark Word被設計成一個非固定的資料結構以便在極小的空間記憶體儲存儘量多的資料,
它會根據物件的狀態複用自己的儲存空間,也就是說,Mark Word會隨著程式的執行發生變化,變化狀態如下
![32位jvm物件儲存圖](https://img2020.cnblogs.com/blog/2197806/202012/2197806-20201213220032716-604169058.png)
但是如果物件是陣列型別,則需要三個機器碼,因為JVM虛擬機器可以通過Java物件的元資料資訊確定Java物件的大小,
但是無法從陣列的元資料來確認陣列的大小,所以用一塊來記錄陣列長度.
在此提出一個問題:程式中,例項物件記憶體 儲存在哪?
很多人瞭解到的都是例項物件儲存在 堆記憶體 中,確實,基本上例項物件記憶體都是存在堆記憶體中的
如果例項物件儲存在堆區時:例項物件記憶體存在堆區,例項的引用存在棧上,例項的元資料class存在方法區或者元空間
但實際上Object例項物件是不一定是存在堆區的,如果例項物件發生了 執行緒逃逸行為 則其記憶體將可能存在 執行緒棧中
下面就這個問題來分析一下
# 逃逸分析
使用逃逸分析的情況,編譯器可以對程式碼做如下優化
一、同步省略。
如果一個物件被發現只能從一個執行緒被訪問到,那麼對於這個物件的操作可以不考慮同步。
二、將堆分配轉化為棧分配。
如果一個物件在子程式中被分配,要使指向該物件的指標永遠不會逃逸,物件可能是棧分配的候選,而不是堆分配。
三、分離物件或標量替換。
有的物件可能不需要作為一個連續的記憶體結構存在也可以被訪問到,那麼物件的部分(或全部)可以不儲存在記憶體,
而是儲存在CPU暫存器中。
在Java程式碼執行時,通過JVM引數可指定是否開啟逃逸分析,
XX:+DoEscapeAnalysis : 表示開啟逃逸分析
XX:DoEscapeAnalysis :表示關閉逃逸分析
從jdk 1.7開始已經預設開始逃逸分析,如需關閉,需要指定XX:DoEscapeAnalysis
逃逸分析程式碼
/**
* 執行緒逃逸 分析
* @author njw
*/
public class StackAllocTest {
/**
* 進行兩種測試
* 1. 關閉逃逸分析,同時調大堆空間,避免堆內GC的發生,如果有GC資訊將會被打印出來
* VM執行引數:-Xmx4G -Xms4G -XX:-DoEscapeAnalysis -XX:+PrintGCDetails -XX:+HeapDumpOnOutOfMemoryError
*
* 以管理員方式執行 power shell
* jps 檢視程序 :6080 StackAllocTest
* jmap -histo 6080
* 結果
* 1: 740 70928456 [I
* 2: 500000 12000000 com.it.edu.sample.StackAllocTest$TestStudent
* 50W個物件
*
*
*
* 2. 開啟逃逸分析
* VM執行引數:-Xmx4G -Xms4G -XX:+DoEscapeAnalysis -XX:+PrintGCDetails -XX:+HeapDumpOnOutOfMemoryError
*
* 結果
* 1: 740 79444704 [I
* 2: 145142 3483408 com.it.edu.sample.StackAllocTest$TestStudent
* 只有145142個
*
*/
public static void main(String[] args) {
long start = System.currentTimeMillis();
for (int i = 0; i < 500000; i++) {
alloc();
}
long end = System.currentTimeMillis();
//檢視執行時間
System.out.println("cost-time " + (end - start) + " ms");
try {
Thread.sleep(100000);
} catch (InterruptedException e1) {
e1.printStackTrace();
}
}
/**
* 在主執行緒中不停建立TestStudent 按照正常邏輯思考 迴圈50W次,建立後堆區 裡面就會有50W的物件
* 如果堆區裡面遠遠小於50W個 可能物件就存在當前執行緒棧中
* 考慮到是否發生GC,當前把GC回收日記打印出來,並同時調大堆空間,避免堆內GC的發生
*
* 存在棧中的原因:
* Jit對編譯時會對程式碼進行 逃逸分析()
* 並不是所有物件存放在堆區,有的一部分存線上程棧空間
* @return
*/
private static TestStudent alloc() {
TestStudent student = new TestStudent();
return student;
}
static class TestStudent {
private String name;
private int age;
}
}
3.局面內建鎖的升級
![JVM鎖升級是過程圖](https://img2020.cnblogs.com/blog/2197806/202012/2197806-20201213220227632-1839289934.png)
JDK1.6版本之後對synchronized的實現進行了各種優化,如自旋鎖、偏向鎖和輕量級鎖,並預設開啟偏向鎖
開啟偏向鎖:-XX:+UseBiasedLocking -XX:BiasedLockingStartupDelay=0
關閉偏向鎖:-XX:-UseBiasedLocking
![JVM鎖升級不可逆](https://img2020.cnblogs.com/blog/2197806/202012/2197806-20201213220257745-726265136.png)
鎖的狀態總共有四種,無鎖狀態、偏向鎖、輕量級鎖和重量級鎖。
隨著鎖的競爭,鎖可以從偏向鎖升級到輕量級鎖,再升級的重量級鎖,但是鎖的升級是單向的,也就是說只能從低到高升級,不會出現鎖的降級。
下圖為鎖的升級全過程
![JVM鎖升級-執行緒間交替執行輕量級鎖過程](https://img2020.cnblogs.com/blog/2197806/202012/2197806-20201213220319397-976278509.png)
jvm鎖的升級詳解
![JVM鎖升級是過程圖](https://img2020.cnblogs.com/blog/2197806/202012/2197806-20201213220342696-725163302.png)
32位jvm物件儲存圖
![ 32位jvm物件儲存圖](https://img2020.cnblogs.com/blog/2197806/202012/2197806-20201213220415496-6528864.png)
JVM鎖的膨脹升級_無鎖到重量級鎖
![JVM鎖的膨脹升級_無鎖到重量級鎖](https://img2020.cnblogs.com/blog/2197806/202012/2197806-20201213220507909-817642028.png)
偏向鎖
偏向鎖是Java 6之後加入的新鎖,它是一種針對加鎖操作的優化手段,經過研究發現,在大多數情況下,鎖不僅不存在多執行緒競爭,
而且總是由同一執行緒多次獲得,因此為了減少同一執行緒獲取鎖(會涉及到一些CAS操作,耗時)的代價而引入偏向鎖。
偏向鎖的核心思想是,
如果一個執行緒獲得了鎖,那麼鎖就進入偏向模式,此時Mark Word 的結構也變為偏向鎖結構,當這個執行緒再次請求鎖時,無需
再做任何同步操作,即獲取鎖的過程,這樣就省去了大量有關鎖申請的操作,從而也就提供程式的效能。
所以,對於沒有鎖競爭的場合,偏向鎖有很好的優化效果,畢竟極有可能連續多次是同一個執行緒申請相同的鎖。
但是對於鎖競爭比較激烈的場合,偏向鎖就失效了,因為這樣場合極有可能每次申請鎖的執行緒都是不相同的,因此這種場合下不應該使用偏向鎖,
否則會得不償失,需要注意的是,偏向鎖失敗後,並不會立即膨脹為重量級鎖,而是先升級為輕量級鎖。
輕量級鎖
倘若偏向鎖失敗,虛擬機器並不會立即升級為重量級鎖,
適用於執行緒交替執行,同步程式碼邏輯少 所需執行執行時間比較少的
它還會嘗試使用一種稱為輕量級鎖的優化手段(1.6之後加入的),此時Mark Word 的結構也變為輕量級鎖的結構。
輕量級鎖能夠提升程式效能的依據是“對絕大部分的鎖,在整個同步週期內都不存在競爭”,注意這是經驗資料。
需要了解的是,輕量級鎖所適應的場景是 **執行緒交替執行同步塊** 的場合,如果存在同一時間訪問同一鎖的場合,
就會導致輕量級鎖膨脹為重量級鎖
自旋鎖
輕量級鎖失敗後,虛擬機器為了避免執行緒真實地在作業系統層面掛起,還會進行一項稱為自旋鎖的優化手段。
這是基於在大多數情況下,執行緒持有鎖的時間都不會太長,
如果直接掛起作業系統層面的執行緒可能會得不償失,畢竟作業系統實現執行緒之間的切換時需要從使用者態轉換到核心態,
這個狀態之間的轉換需要相對比較長的時間,時間成本相對較高,因此自旋鎖會假設在不久將來,當前的執行緒可以獲得鎖,
因此虛擬機器會讓當前想要獲取鎖的執行緒做幾個空迴圈(這也是稱為自旋的原因),一般不會太久,可能是50個迴圈或100迴圈,
在經過若干次迴圈後,如果得到鎖,就順利進入臨界區。如果還不能獲得鎖,那就會將執行緒在作業系統層面掛起,
這就是自旋鎖的優化方式,這種方式確實也是可以提升效率的。最後沒辦法也就只能升級為重量級鎖了
鎖消除 和 鎖的粗化
消除鎖是虛擬機器另外一種鎖的優化,這種優化更徹底,
Java虛擬機器在JIT編譯時(可以簡單理解為當某段程式碼即將第一次被執行時進行編譯,又稱即時編譯),通過對執行上下文的掃描,
去除不可能存在共享資源競爭的鎖,通過這種方式消除沒有必要的鎖,可以節省毫無意義的請求鎖時間,
例如說
StringBuffer的append是一個同步方法,但是在add方法中的StringBuffer屬於一個區域性變數,
並且不會被其他執行緒所使用,因此StringBuffer不可能存在共享資源競爭的情景,JVM會自動將其鎖消除
程式碼分析,鎖的粗化和消除
/**
*
* JVM對鎖的優化
* 1.鎖的粗化
* 2.鎖的消除
*
* @author njw
*/
public class Test {
StringBuffer stb = new StringBuffer();
/**
* 鎖的粗化
*
* StringBuffer 呼叫 append的時候,鎖加在當前物件上
* 按照正常邏輯思考 下面呼叫了 四次 append,相當於加了四個同步塊
* synchronized{
* stb.append("1");
* }
* synchronized{
* stb.append("2");
* }
* ...
*
* 如果是這樣意味著這次操作要進行四次上下文切換,四次加鎖,四次釋放鎖
*
* 但是jvm經過優化,會把 四個變成一個,加成了一個統一的全域性鎖 這就是鎖的粗化
* synchronized{
* stb.append("1");
* stb.append("2");
* }
*
*/
public void test1(){
//jvm的優化,鎖的粗化
stb.append("1");
stb.append("2");
stb.append("3");
stb.append("4");
}
/**
* 鎖的消除
*
* synchronized (new Object()) {
* //虛擬碼:很多邏輯
* }
* jvm是否會對上面程式碼進行加鎖?
* 答案 這裡jvm不會對這同步塊進行加鎖
*
* 這裡的程式碼中 jvm會進行逃逸分析
* 因為:new Object()這個加鎖物件中,這個new Object()並不會被其他執行緒訪問到,加鎖並沒有意義,不會產生執行緒 逃逸
* 所以這裡不會加鎖 這便是 JVM 鎖的消除
*
* 具體情況檢視 逃逸分析 優化
*
*/
public void test2(){
//jvm的優化,JVM不會對同步塊進行加鎖
synchronized (new Object()) {
//虛擬碼:很多邏輯
//jvm是否會加鎖?
//jvm會進行逃逸分析
}
}
public static void main(String[] args) {
Test test = new Test();
}
}
# 2.1 同步框架AbstractQueuedSynchronizer
Java併發程式設計核心在於java.concurrent.util包
而juc當中的大多數同步器實現都是圍繞著共同的基礎行為,比如 等待佇列、條件佇列、獨佔獲取(排他鎖)、共享獲取(共享) 等
而這個行為的抽象就是基於AbstractQueuedSynchronizer簡稱AQS
AQS定義了一套多執行緒訪問共享資源的同步器框架,是一個依賴狀態(state)的同步器
state 會記錄加鎖狀態、次數等 ,使框架有了可重複入的特性
獨佔獲取 抽象除了排他鎖
共享獲取 抽象除了共享鎖
等待佇列,條件佇列 使其具備了公平、非公平特性
AQS具備特性
阻塞等待佇列
共享/獨佔
公平/非公平
可重入
允許中斷
例如Java.concurrent.util當中同步器的實現如Lock,Latch,Barrier等,都是基於AQS框架實現
一般通過定義內部類Sync繼承AQS
將同步器所有呼叫都對映到Sync對應的方法
1. AQS內部維護屬性volatile int state (32位)
state表示資源的可用狀態
2. State三種訪問方式
getState()、setState()、compareAndSetState()
3. AQS定義兩種資源共享方式
Exclusive-獨佔,只有一個執行緒能執行,如ReentrantLock
Share-共享,多個執行緒可以同時執行,如Semaphore/CountDownLatch
4. AQS定義兩種佇列
同步等待佇列
條件等待佇列
不同的自定義同步器爭用共享資源的方式也不同。
自定義同步器在實現時只需要實現共享資源state的獲取與釋放方式即可,至於具體執行緒等待佇列的維護(如獲取資源失敗入隊/喚醒出隊等),
AQS已經在頂層實現好了。
自定義同步器實現時主要實現以下幾種方法:
isHeldExclusively():該執行緒是否正在獨佔資源。只有用到condition才需要去實現它。
tryAcquire(int):獨佔方式。嘗試獲取資源,成功則返回true,失敗則返回false。
tryRelease(int):獨佔方式。嘗試釋放資源,成功則返回true,失敗則返回false。
tryAcquireShared(int):共享方式。嘗試獲取資源。負數表示失敗;0表示成功,但沒有剩餘可用資源;正數表示成功,且有剩餘資源。
tryReleaseShared(int):共享方式。嘗試釋放資源,如果釋放後允許喚醒後續等待結點返回true,否則返回false。
# 2.2 同步等待佇列
CLH佇列是Craig、Landin、Hagersten三人發明的一種基於雙向連結串列資料結構的佇列,
是FIFO先入先出執行緒等待佇列,Java中的CLH佇列是原CLH佇列的一個變種,執行緒由原自旋機制改為阻塞機制
![CLH佇列圖](https://img2020.cnblogs.com/blog/2197806/202012/2197806-20201213220613234-1918489838.png)
![等待佇列圖解](https://img2020.cnblogs.com/blog/2197806/202012/2197806-20201213221233390-1610808834.png)
# 2.3 條件等待佇列
Condition是一個多執行緒間協調通訊的工具類,使得某個,或者某些執行緒一起等待某個條件(Condition),
只有當該條件具備時 ,這些等待執行緒才會被喚醒,從而重新爭奪鎖
![條件佇列圖](https://img2020.cnblogs.com/blog/2197806/202012/2197806-20201213220633122-2138050821.png)
![條件佇列圖解](https://img2020.cnblogs.com/blog/2197806/202012/2197806-20201213221254950-1090645105.png)
# 2.4 公平鎖
![公平鎖-打水圖](https://img2020.cnblogs.com/blog/2197806/202012/2197806-20201213220716705-1442587789.png)
# 2.5 非公平鎖
![非公平鎖-打水圖](https://img2020.cnblogs.com/blog/2197806/202012/2197806-20201213220730803-346973542.png)
# 2.6 重入鎖
![重入鎖-打水圖](https://img2020.cnblogs.com/blog/2197806/202012/2197806-20201213220747482-111472374.png)
# 2.7 非重入鎖
![非重入鎖-打水圖](https://img2020.cnblogs.com/blog/2197806/202012/2197806-20201213220804584-136423281.png)
# 2.8 讀寫鎖
寫鎖(獨享鎖、排他鎖),是指該鎖一次只能被一個執行緒所持有。如果執行緒T對資料A加上排它鎖後,則其他執行緒不能再對A加任何型別的鎖。獲得寫鎖的執行緒即能讀資料又能修改資料。
讀鎖(共享鎖)是指該鎖可被多個執行緒所持有。如果執行緒T對資料A加上共享鎖後,則其他執行緒只能對A再加共享鎖,不能加排它鎖。獲得讀鎖的執行緒只能讀資料,不能修改資料。
AQS中state欄位(int型別,32位),此處state上分別描述讀鎖和寫鎖的數量於是將state變數“按位切割”切分成了兩個部分
高16位表示讀鎖狀態(讀鎖個數)
低16位表示寫鎖狀態(寫鎖個數)
![32位讀寫鎖圖](https://img2020.cnblogs.com/blog/2197806/202012/2197806-20201213220821442-2130160992.png)
#4. ReentrantLock分析
1. ReentrantLock 內部類
1.Node節點介紹
static final class Node {
/**
* 標記節點未共享模式
* */
static final Node SHARED = new Node();
/**
* 標記節點為獨佔模式
*/
static final Node EXCLUSIVE = null;
/**
* 在同步佇列中等待的執行緒等待超時或者被中斷,需要從同步佇列中取消等待
* 在佇列節點構建的時候 假如一個節點加入等待佇列 會在加入的時候檢查其他佇列中旳節點是否處於 這個狀態,如果是的話就剔除,
* 並且繼續檢查其他的?
* */
static final int CANCELLED = 1;
/**
* 後繼節點的執行緒處於等待狀態,而當前的節點如果釋放了同步狀態或者被取消,
* 將會通知後繼節點,使後繼節點的執行緒得以執行。
*
* 此狀態是可以被喚醒的 可以去獲取鎖
*/
static final int SIGNAL = -1;
/**
* 處於等待佇列
* 該狀態說明 節點在等待佇列中,節點的執行緒等待在Condition上,當其他執行緒對Condition呼叫了signal()方法後,
* 該節點會從 等待佇列 中轉移到 同步佇列 中,加入到同步狀態的獲取中
*/
static final int CONDITION = -2;
/**
* 表示下一次共享式同步狀態獲取將會被 無條件地傳播下去
* 假如執行緒t1 執行完之後,廣播發現t2,處於 PROPAGATE 狀態,可以無條件去喚醒,並繼續檢查t3
*/
static final int PROPAGATE = -3;
/**
* 標記當前節點的訊號量狀態 (1,0,-1,-2,-3)5種狀態
* 使用CAS更改狀態,volatile保證執行緒可見性,高併發場景下,
* 即被一個執行緒修改後,狀態會立馬讓其他執行緒可見。
*/
volatile int waitStatus;
/**
* 前驅節點,當前節點加入到同步佇列中被設定
*/
volatile Node prev;
/**
* 後繼節點
*/
volatile Node next;
/**
* 節點同步狀態的執行緒
*/
volatile Thread thread;
/**
* TODO 這個節點用在條件佇列中 訊號燈
*
* 等待佇列中的後繼節點,如果當前節點是共享的,那麼這個欄位是一個SHARED常量,
* 也就是說節點型別 (獨佔和共享)和 等待佇列中 的後繼節點共用同一個欄位。
*/
Node nextWaiter;
/**
* 判斷是否共享
* Returns true if node is waiting in shared mode.
*/
final boolean isShared() {
return nextWaiter == SHARED;
}
/**
* 返回前驅節點
*/
final Node predecessor() throws NullPointerException {
Node p = prev;
if (p == null)
throw new NullPointerException();
else
return p;
}
Node() { // Used to establish initial head or SHARED marker
}
Node(Thread thread, Node mode) { // Used by addWaiter
this.nextWaiter = mode;
this.thread = thread;
}
Node(Thread thread, int waitStatus) { // Used by Condition
this.waitStatus = waitStatus;
this.thread = thread;
}
}
2.FairSync 公平鎖
static final class FairSync extends Sync {
private static final long serialVersionUID = -3000897897090466540L;
@Override
final void lock() {
acquire(1);
}
/**
* 重寫aqs中的方法邏輯
* 嘗試加鎖,被AQS的acquire()方法呼叫
*/
@Override
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
// 表示當前沒有任何執行緒加鎖,可以去加鎖
if (c == 0) {
/**
* 與非公平鎖中的區別,需要先判斷隊列當中是否有等待的節點
* 如果沒有則可以嘗試CAS獲取鎖 : 使用原子操作更新 狀態
* compareAndSetState : 依賴於 unsafe 操作執行原子比較操作
*/
// hasQueuedPredecessors: 判斷是否頭結點不等於尾結點 同時 頭結點的下一個為空,或者頭結點的下一個不是當前執行緒
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
//獨佔執行緒指向當前執行緒
setExclusiveOwnerThread(current);
return true;
}
}
// 狀態已經被修改過了 判斷當前執行緒是否是獲取到的那個 如果是說明在重入
else if (current == getExclusiveOwnerThread()) {
// 重入鎖 新增鎖數量
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
}
![ReentrantLock 二次加鎖圖](https://img2020.cnblogs.com/blog/2197806/202012/2197806-20201213220852382-818697351.png)
![ReentrantLock 多執行緒公平鎖加鎖圖](https://img2020.cnblogs.com/blog/2197806/202012/2197806-20201213220910823-1213452099.jpg)
3.NonfairSync 非公平鎖
NonfairSync 定義
static final class NonfairSync extends Sync {
private static final long serialVersionUID = 7316153563782823691L;
/**
* 加鎖行為
*/
@Override
final void lock() {
/**
* 第一步:直接嘗試加鎖
* 與公平鎖實現的加鎖行為一個最大的區別在於,此處不會去判斷同步佇列(CLH佇列)中是否有排隊等待加鎖的節點,
* 一上來就直接加鎖(判斷state是否為0,CAS修改state為1)
* 並將獨佔鎖持有者 exclusiveOwnerThread 屬性指向當前執行緒
* 如果當前有人佔用鎖,再嘗試去加一次鎖
*/
if (compareAndSetState(0, 1)) {
// 嘗試修改擁有執行緒為當前執行緒
setExclusiveOwnerThread(Thread.currentThread());
} else {
//AQS定義的方法,加鎖
acquire(1);
}
}
/**
* 父類AbstractQueuedSynchronizer.acquire()中呼叫本方法
*/
@Override
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
}
NonfairSync 獲取鎖
/**
* 嘗試獲取非公平鎖
*/
final boolean nonfairTryAcquire(int acquires) {
//acquires = 1
final Thread current = Thread.currentThread();
int c = getState();
/**
* 不需要判斷同步佇列(CLH)中是否有排隊等待執行緒
* 判斷state狀態是否為0,為0可以加鎖
*/
if (c == 0) {
//unsafe操作,cas修改state狀態
if (compareAndSetState(0, acquires)) {
//獨佔狀態鎖持有者指向當前執行緒
setExclusiveOwnerThread(current);
return true;
}
}
/**
* state狀態不為0,判斷鎖持有者是否是當前執行緒,
* 如果是當前執行緒持有 則state+1
*/
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;
}
AQS定義的方法,加鎖
public final void acquire(int arg) {
// tryAcquire 實際呼叫的子類方法
if (!tryAcquire(arg) &&
// addWaiter 首先新增一個節點在佇列中 新增到尾部
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
addWaiter 新增節點:
private Node addWaiter(Node mode) {
// 1. 將當前執行緒構建成Node型別
Node node = new Node(Thread.currentThread(), mode);
// Try the fast path of enq; backup to full enq on failure
Node pred = tail;
// 2. 判斷 當前尾節點是否為null?
if (pred != null) {
// 2.2 將當前節點尾插入的方式,插入到尾部
// 將新創的結點的prev(前驅節點)指向原本的tail節點
node.prev = pred;
// 2.3 使用CAS將節點插入同步佇列的尾部
if (compareAndSetTail(pred, node)) {
// 如果插入成功 把原本的tail的下一個節點指向 當前新建的結點 然後返回當前節點
pred.next = node;
return node;
}
}
// 把節點加入CLH同步佇列 主要是 單前tail 是空的話 上面的邏輯沒執行到,裡面有個類似的結點指向操作
enq(node);
return node;
}
/**
* 節點加入CLH同步佇列
*/
private Node enq(final Node node) {
for (;;) {
Node t = tail;
if (t == null) { // Must initialize
//佇列為空需要初始化,建立空的頭節點
if (compareAndSetHead(new Node()))
tail = head;
} else {
// 佇列中已經有值 尾節點不是空 把當前傳進來的結點的 prev節點指向 當前tail節點
node.prev = t;
//set尾部節點
if (compareAndSetTail(t, node)) {//當前節點置為尾部
t.next = node; //前驅節點的next指標指向當前節點
return t;
}
}
}
}
acquireQueued:
/**
* 已經在隊列當中的Thread節點,準備阻塞等待獲取鎖
*/
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
//死迴圈自旋
for (;;) {
//找到當前結點的前驅結點
final Node p = node.predecessor();
// 如果前驅結點是頭結點,才tryAcquire,其他結點是沒有機會tryAcquire的。
if (p == head && tryAcquire(arg)) {
//獲取同步狀態成功,將當前結點設定為頭結點。
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
/**
* 如果前驅節點不是Head,通過shouldParkAfterFailedAcquire判斷是否應該阻塞
* 前驅節點訊號量為-1,當前執行緒可以安全被parkAndCheckInterrupt用來阻塞執行緒
*/
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt()) {
interrupted = true;
}
}
} finally {
if (failed) {
cancelAcquire(node);
}
}
}
parkAndCheckInterrupt:
/**
* 阻塞當前節點,返回當前Thread的中斷狀態
* LockSupport.park 底層實現邏輯呼叫系統核心功能 pthread_mutex_lock 阻塞執行緒
*/
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);//阻塞
return Thread.interrupted();
}
shouldParkAfterFailedAcquire:
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
//判斷是否應該阻塞
// 獲取前驅節點等待狀態
int ws = pred.waitStatus;
// 此狀態是可以被喚醒的 可以去獲取鎖
if (ws == Node.SIGNAL)
/*
* 若前驅結點的狀態是SIGNAL,意味著當前結點可以被安全地park
*/
return true;
if (ws > 0) {
/* 狀態是 1 被移除,並且繼續檢查其他節點,如果都是取消狀態 一併移除
* 前驅節點狀態如果被取消狀態,將被移除出佇列
*/
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
/* 同步佇列不會出現 CONDITION
* 所以 當前驅節點waitStatus為 0 or PROPAGATE(可傳遞狀態)狀態時
*
* 將其設定為SIGNAL狀態,然後當前結點才可以可以被安全地park
*/
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
![ReentrantLock 圖解](https://img2020.cnblogs.com/blog/2197806/202012/2197806-20201213220948921-885100