上一篇文章中咱們一塊兒學習了jvm緩存一致性、多線程間的原子性、有序性、指令重排的相關內容,
這一篇文章便開始和你們一塊兒學習學習AQS(AbstractQueuedSynchronizer)的內容
主要是包含如下三個方面java
synchronized ReentrantLock AbstractQueuedSynchronizer
1.瞭解併發同步器node
多線程編程中,有可能會出現多個線程同時訪問同一個共享、可變資源的狀況;這種資源多是:對象、變量、文件等。 共享:資源能夠由多個線程同時訪問 可變:資源能夠在其生命週期內被修改 由此能夠得出 因爲線程執行的過程是不可控的,因此須要採用同步機制來協同對對象可變狀態的訪問
那麼咱們怎麼解決線程併發安全問題?spring
實際上,全部的併發模式在解決線程安全問題時,採用的方案都是序列化訪問臨界資源。 即在同一時刻,只能有一個線程訪問臨界資源,也稱做同步互斥訪問。 Java 中,提供了兩種方式來實現同步互斥訪問:synchronized和Lock
同步器的本質就是加鎖shell
加鎖目的:序列化訪問臨界資源,即同一時刻只能有一個線程訪問臨界資源(同步互斥訪問) 不過有一點須要區別的是: 當多個線程執行一個方法時,該方法內部的局部變量 並非臨界資源, 由於這些局部變量是在每一個線程的私有棧中,所以不具備共享性,不會致使線程安全問題
其中鎖包括 顯式鎖 和 隱式鎖編程
顯式: 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 兩條指令分別在同步塊邏輯代碼的起始位置與結束位置
每一個同步對象都有一個本身的Monitor(監視器鎖),加鎖過程以下圖所示:
那麼有個問題來了,咱們知道synchronized加鎖加在對象上,對象是如何記錄鎖狀態的呢?多線程
答案是鎖狀態是被記錄在每一個對象的對象頭(Mark Word)中,下面咱們一塊兒認識一下對象的內存佈局
HotSpot虛擬機中,對象在內存中存儲的佈局能夠分爲三塊區域:
對象頭(Header)、實例數據(Instance Data)和對齊填充(Padding)。 對象頭:好比 hash碼,對象所屬的年代,對象鎖,鎖狀態標誌,偏向鎖(線程)ID,偏向時間,數組長度(數組對象)等 實例數據:即建立對象時,對象中成員變量,方法等 對齊填充:對象的大小必須是8字節的整數倍
對象頭
HotSpot虛擬機的對象頭包括兩部分信息,第一部分是「Mark Word」,用於存儲對象自身的運行時數據, 如哈希碼(HashCode)、GC分代年齡、鎖狀態標誌、線程持有的鎖、偏向線程ID、偏向時間戳等等, 這部分數據的長度在32位和64位的虛擬機(暫 不考慮開啓壓縮指針的場景)中分別爲32個和64個Bits, 官方稱它爲「Mark Word」。對象須要存儲的運行時數據不少,其實已經超出了3二、64位Bitmap結構所能記錄的限度, 可是對象頭信息是與對象自身定義的數據無關的額 外存儲成本,考慮到虛擬機的空間效率, Mark Word被設計成一個非固定的數據結構以便在極小的空間內存儲儘可能多的信息,它會根據對象的狀態複用本身的存儲空間。 例如在32位的HotSpot虛擬機中對象未被鎖定的狀態下,Mark Word的32個Bits空間中的25Bits用於存儲對象哈希碼(HashCode), 4Bits用於存儲對象分代年齡,2Bits用於存儲鎖標誌位,1Bit固定爲0, 對象頭信息是與對象自身定義的數據無關的額外存儲成本,可是考慮到虛擬機的空間效率,Mark Word被設計成一個非固定的數據結構以便在極小的空間內存存儲儘可能多的數據, 它會根據對象的狀態複用本身的存儲空間,也就是說,Mark Word會隨着程序的運行發生變化,變化狀態以下
可是若是對象是數組類型,則須要三個機器碼,由於JVM虛擬機能夠經過Java對象的元數據信息肯定Java對象的大小, 可是沒法從數組的元數據來確認數組的大小,因此用一塊來記錄數組長度.
在此提出一個問題:程序中,實例對象內存 存儲在哪?
不少人瞭解到的都是實例對象存儲在 堆內存 中,確實,基本上實例對象內存都是存在堆內存中的
若是實例對象存儲在堆區時:實例對象內存存在堆區,實例的引用存在棧上,實例的元數據class存在方法區或者元空間
但實際上Object實例對象是不必定是存在堆區的,若是實例對象發生了 線程逃逸行爲 則其內存將可能存在 線程棧中
下面就這個問題來分析一下
使用逃逸分析的狀況,編譯器能夠對代碼作以下優化
1、同步省略。 若是一個對象被發現只能從一個線程被訪問到,那麼對於這個對象的操做能夠不考慮同步。 2、將堆分配轉化爲棧分配。 若是一個對象在子程序中被分配,要使指向該對象的指針永遠不會逃逸,對象多是棧分配的候選,而不是堆分配。 3、分離對象或標量替換。 有的對象可能不須要做爲一個連續的內存結構存在也能夠被訪問到,那麼對象的部分(或所有)能夠不存儲在內存, 而是存儲在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.局面內置鎖的升級
JDK1.6版本以後對synchronized的實現進行了各類優化,如自旋鎖、偏向鎖和輕量級鎖,並默認開啓偏向鎖
開啓偏向鎖:-XX:+UseBiasedLocking -XX:BiasedLockingStartupDelay=0 關閉偏向鎖:-XX:-UseBiasedLocking
鎖的狀態總共有四種,無鎖狀態、偏向鎖、輕量級鎖和重量級鎖。
隨着鎖的競爭,鎖能夠從偏向鎖升級到輕量級鎖,再升級的重量級鎖,可是鎖的升級是單向的,也就是說只能從低到高升級,不會出現鎖的降級。
下圖爲鎖的升級全過程
jvm鎖的升級詳解
32位jvm對象存儲圖
JVM鎖的膨脹升級_無鎖到重量級鎖
偏向鎖
偏向鎖是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(); } }
Java併發編程核心在於java.concurrent.util包
而juc當中的大多數同步器實現都是圍繞着共同的基礎行爲,好比 等待隊列、條件隊列、獨佔獲取(排他鎖)、共享獲取(共享) 等
而這個行爲的抽象就是基於AbstractQueuedSynchronizer簡稱AQS
AQS定義了一套多線程訪問共享資源的同步器框架,是一個依賴狀態(state)的同步器
state 會記錄加鎖狀態、次數等 ,使框架有了可重複入的特性 獨佔獲取 抽象除了排他鎖 共享獲取 抽象除了共享鎖 等待隊列,條件隊列 使其具有了公平、非公平特性
AQS具有特性
阻塞等待隊列 共享/獨佔 公平/非公平 可重入 容許中斷
例如Java.concurrent.util當中同步器的實現如Lock,Latch,Barrier等,都是基於AQS框架實現
通常經過定義內部類Sync繼承AQS 將同步器全部調用都映射到Sync對應的方法
AQS內部維護屬性volatile int state (32位)
state表示資源的可用狀態
State三種訪問方式
getState()、setState()、compareAndSetState()
AQS定義兩種資源共享方式
Exclusive-獨佔,只有一個線程能執行,如ReentrantLock
Share-共享,多個線程能夠同時執行,如Semaphore/CountDownLatch
AQS定義兩種隊列
同步等待隊列
條件等待隊列
不一樣的自定義同步器爭用共享資源的方式也不一樣。
自定義同步器在實現時只須要實現共享資源state的獲取與釋放方式便可,至於具體線程等待隊列的維護(如獲取資源失敗入隊/喚醒出隊等),
AQS已經在頂層實現好了。
自定義同步器實現時主要實現如下幾種方法:
isHeldExclusively():該線程是否正在獨佔資源。只有用到condition才須要去實現它。 tryAcquire(int):獨佔方式。嘗試獲取資源,成功則返回true,失敗則返回false。 tryRelease(int):獨佔方式。嘗試釋放資源,成功則返回true,失敗則返回false。 tryAcquireShared(int):共享方式。嘗試獲取資源。負數表示失敗;0表示成功,但沒有剩餘可用資源;正數表示成功,且有剩餘資源。 tryReleaseShared(int):共享方式。嘗試釋放資源,若是釋放後容許喚醒後續等待結點返回true,不然返回false。
CLH隊列是Craig、Landin、Hagersten三人發明的一種基於雙向鏈表數據結構的隊列,
是FIFO先入先出線程等待隊列,Java中的CLH隊列是原CLH隊列的一個變種,線程由原自旋機制改成阻塞機制
Condition是一個多線程間協調通訊的工具類,使得某個,或者某些線程一塊兒等待某個條件(Condition),
只有當該條件具有時 ,這些等待線程纔會被喚醒,從而從新爭奪鎖
寫鎖(獨享鎖、排他鎖),是指該鎖一次只能被一個線程所持有。若是線程T對數據A加上排它鎖後,則其餘線程不能再對A加任何類型的鎖。得到寫鎖的線程即能讀數據又能修改數據。 讀鎖(共享鎖)是指該鎖可被多個線程所持有。若是線程T對數據A加上共享鎖後,則其餘線程只能對A再加共享鎖,不能加排它鎖。得到讀鎖的線程只能讀數據,不能修改數據。 AQS中state字段(int類型,32位),此處state上分別描述讀鎖和寫鎖的數量因而將state變量「按位切割」切分紅了兩個部分 高16位表示讀鎖狀態(讀鎖個數) 低16位表示寫鎖狀態(寫鎖個數)
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; } }
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; }