concurrent包概述
concurrent包結構
concurrent包類圖結構
concurrent包綜述
- 綜述: 在整個併發包設計上,Doug Lea大師採用了3.1 Concurrent包整體架構的三層結構
- 補充: 併發包所涉及的內容筆者會陸續推出對應番進行闡述,敬請期待(進度視筆者的忙碌程度而定)
1. 底層-硬件指令支持
- 綜述: 併發包最底層是依賴於硬件級別的Volatile和CAS的支持
- Volatile:借用 Volatile 的內存讀寫語義和阻止重排序保證數據可見性
- CAS: 借用CAS的高效機器級別原子指令保證內存執行的 讀-改-寫 操作的原子性
- 組合: 借用 Volatile 變量的讀/寫和CAS實現線程之間的有效通信,保證了原子性、可見性、有序性
2. 中間層-基礎數據結構+算法支持
- 綜述: 在數據結構和算法的設計使用上,Doug Lea大師專門設計了AQS框架作爲所有併發類庫的併發基礎,同時引入非阻塞算法和原子變量類增強了併發特性
- AQS框架: AQS中提供了最基本、有效的併發API, Doug Lea大師期望其作爲所有併發操作的基礎解決方案,併發包中的絕大部分實現都是依賴於AQS(AbstractQueuedSynchronizer),同時 AQS的基礎是 CAS 和 Volatile的底層支持
- 非阻塞數據結構: 非阻塞數據結構是非阻塞隊列的設計基礎,同時也是阻塞隊列的參考對比的重要依據
- 原子變量類: Doug Lea大師專門爲所有的原子變量設計了專門的類庫,甚至在後期還對齊做了增強,比如 LongAdder、LongAccumulator 等,從側面可以反映出數值操作對於編程的重要性
3. 高層-併發類庫支持
- 綜述: Doug Lea大師在併發包中已經提供了豐富的併發類庫極大方便了快速、安全的使用併發操作
- Lock: Lock接口定義了一系列併發操作標準,詳情參見 AQS框架之Lock
- 同步器: 每個併發類的同步器的實現依賴於AQS(繼承),比如 ReentrantLock 中的Sync;同時筆者也將 併發類 同屬於同步器的範圍內
- 阻塞隊列: 顧名思義,支持阻塞的隊列,主要是以Queue結尾的類
- 執行器: 所謂執行器,指的是任務的執行者,比如線程池和Fork-Join
- 併發容器: 即支持併發的容器,主要包含COW和以Concurrent開頭的類,通常併發容器是非阻塞的
Lock接口
synchronized的不足
- 不可中斷:使用內部鎖(指的是 synchronized) 時,不能中斷正在等待獲取鎖的線程
- 不可超時:使用內部鎖時,在請求鎖失敗情況下,必須無限等待,沒有超時效果
- 自動釋放:使用內部鎖時,內部鎖必須在獲取它們的代碼塊中被自動釋放(雖然對代碼來說是種簡化且對異常友好)
- 不可伸縮:使用內部鎖時,無法細粒度控制鎖(伸縮性不足),即無法實現鎖分離和鎖聯結,比如爲每個鏈表節點(或部分)加鎖從而允許不同的線程能夠獨立操作鏈表的不同節點(部分),遍歷或修改鏈表時,需先獲取該節點鎖並直到獲取下一個節點鎖時才釋放當前節點鎖
- 性能問題:使用內部鎖時,在有競爭情況下仍會出現性能問題,儘管JDK6對內部鎖進行了優化,但無論是偏向鎖或是輕量級鎖都是針對無競爭情況的優化,無競爭情況下與 ReentractLock 性能一致,但有競爭時Lock明顯更高效
Lock接口綜述
- 定義: JDK1.5 引入Lock接口,其定義了一些抽象的鎖操作,相比synchronized,Lock 提供了無條件、可輪詢、可定時、可中斷的鎖獲取操作,所有加鎖和解鎖的方法都是顯式的
- 實現: Lock 的實現必須提供具有與 synchronized 相同的內存語義,但加鎖的語義、調度算法、順序保證、性能特性可以有所不同
- 使用: Lock接口的實現基本是通過聚合一個同步器 AbstractQueuedSynchronized 的子類來完成線程的訪問控制
- 對比內部鎖: Lock缺少隱式獲取/釋放鎖的便捷,但卻擁有了鎖獲取與釋放的可操作性、可中斷的獲取鎖以及超時獲取鎖等多種內部鎖不具備的同步性,甚至還支持讀寫鎖分離,同時允許獲取和釋放可以不在同一個塊中
- lock接口對應實現在concurrent包下的locks包內,結構如下圖:
lock接口優勢:
lock接口方法
lock()
- lock方法應具有與內部鎖加鎖相同的內存語義,即無鎖阻塞和支持可重入
- lock方法必須搭配unlock方法使用,同時必須在finally中顯式調用unlock方法釋放鎖
/**
* Acquires the lock.
* 獲取鎖,調用該方法的當前線程將會獲取鎖,當鎖獲得後,從該方法返回
* <p>If the lock is not available then the current thread becomes
* disabled for thread scheduling purposes and lies dormant until the
* lock has been acquired.
* 若當前鎖不可用(已被佔有),當前線程會一直休眠直到鎖爲可被獲取狀態
* <p><b>Implementation Considerations</b>
* 實現該方法的注意事項
* <p>A {@code Lock} implementation may be able to detect erroneous use
* of the lock, such as an invocation that would cause deadlock, and
* may throw an (unchecked) exception in such circumstances. The
* circumstances and the exception type must be documented by that
* {@code Lock} implementation.
* 該方法的實現需要能發現lock被錯誤使用,如死鎖或拋出不可查異常(即可運行期異常和Error)
* 此時該實現必須用文檔註明其可能出現的異常或需要的使用環境
*/
void lock();
lockInterruptibly()
- lockInterruptibly 方法提供可中斷的鎖獲取操作並允許在可取消的活動中使用
說明:
/**
* Acquires the lock unless the current thread is
* {@linkplain Thread#interrupt interrupted}.
* 可中斷地獲取鎖,即在鎖的獲取中可以中斷當前線程
* <p>Acquires the lock if it is available and returns immediately.
* 當獲取鎖時鎖可用就立即返回
* <p>If the lock is not available then the current thread becomes
* disabled for thread scheduling purposes and lies dormant until
* one of two things happens:
* <ul>
* <li>The lock is acquired by the current thread; or
* <li>Some other thread {@linkplain Thread#interrupt interrupts} the
* current thread, and interruption of lock acquisition is supported.
* </ul>
* 若當前鎖不可用(已被佔有),當前線程會一直休眠直到以下兩種情況發生:
* 1.鎖被當前線程獲取
* 2.其他線程中斷當前線程,同時鎖的獲取允許被中斷
* <p><b>Implementation Considerations</b>
* 實現該方法的注意事項
* <p>The ability to interrupt a lock acquisition in some
* implementations may not be possible, and if possible may be an
* expensive operation. The programmer should be aware that this
* may be the case. An implementation should document when this is
* the case.
* 該方法屬於拓展方法,只有需要中斷服務的時候才需要實現它
* <p>An implementation can favor responding to an interrupt over
* normal method return.
* 相對於返回,該方法更適合拋出一箇中斷響應,比如中斷異常
* @throws InterruptedException if the current thread is
* interrupted while acquiring the lock (and interruption
* of lock acquisition is supported)
*/
void lockInterruptibly() throws InterruptedException;
//樣例代碼
public boolean doTask throws InterruptedException(){
lock.lockInterruptibly();
try{
return cancelTask();
}finally{
lock.unlock();
}
}
//取消任務
private boolean cancelTask() throws InterruptedException {...}
tryLock()
- tryLock 方法提供可定時與可輪詢的鎖獲取方式,與無條件的鎖獲取相比,具有更完善的錯誤恢復機制
- tryLock 方法能夠有效的防止死鎖的發生,比如使用輪詢鎖優雅失敗規避死鎖
- tryLock 方法同時提供定時鎖的功能,其允許在限時活動內部使用獨佔鎖,當線程獲取鎖、被中斷或超時後返回
- tryLock 方法支持輪詢獲取鎖:通過一個循環配合tryLock()來不斷嘗試獲取鎖,由於tryLock()非阻塞因此會立即返回是否成功獲取鎖的結果;當不能獲取所有的鎖時,應釋放已獲得的所有鎖並重新嘗試獲取
- tryLock 方法同時支持響應中斷
/**
* Acquires the lock only if it is free at the time of invocation.
* 嘗試非阻塞的獲取鎖,調用該方法後立即返回是否成功獲取鎖true/false
* <p>Acquires the lock if it is available and returns immediately
* with the value {@code true}.
* If the lock is not available then this method will return
* immediately with the value {@code false}.
* 當鎖不可用時立即返回false
* This usage ensures that the lock is unlocked if it was acquired, and
* doesn't try to unlock if the lock was not acquired.
* 該實現應確保當鎖被獲取時是未鎖狀態,當未被獲取時不會嘗試解鎖
* @return {@code true} if the lock was acquired and
* {@code false} otherwise
*/
boolean tryLock();
/**
* Acquires the lock if it is free within the given waiting time and the
* current thread has not been {@linkplain Thread#interrupt interrupted}.
* 沒有被中斷當前線程在指定超時時間內獲取鎖
* If the lock is not available then the current thread becomes disabled for
* thread scheduling purposes and lies dormant until one of three things happens:
* <ul>
* <li>The lock is acquired by the current thread; or
* <li>Some other thread {@linkplain Thread#interrupt interrupts} the
* current thread, and interruption of lock acquisition is supported; or
* <li>The specified waiting time elapses
* </ul>
* <p>If the specified waiting time elapses then the value {@code false} is returned.
* If the time is less than or equal to zero, the method will not wait at all.
* 當前線程在以下三種情況下會返回:
* 1.當前線程在超時時間內獲得鎖
* 2.當前線程在超時時間內被中斷
* 3.超時時間結束,返回false,線程不再被阻塞
* @param time the maximum time to wait for the lock
* @param unit the time unit of the {@code time} argument
* @return {@code true} if the lock was acquired and {@code false}
* if the waiting time elapsed before the lock was acquired
* @throws InterruptedException if the current thread is interrupted
* while acquiring the lock (and interruption of lock
* acquisition is supported)
*/
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
CAS
CAS,compare and swap的縮寫,中文翻譯成比較並交換
在java語言之前,併發就已經廣泛存在並在服務器領域得到了大量的應用。所以硬件廠商老早就在芯片中加入了大量直至併發操作的原語,從而在硬件層面提升效率。在intel的CPU中,使用cmpxchg指令。
CAS 操作包含三個操作數 —— 內存位置(V)、預期原值(A)和新值(B)。 如果內存位置的值與預期原值相匹配,那麼處理器會自動將該位置值更新爲新值 。否則,處理器不做任何操作。
CAS原理
利用CPU的CAS指令,同時藉助JNI來完成Java的非阻塞算法。其它原子操作都是利用類似的特性完成的。
整個J.U.C都是建立在CAS之上的,因此對於synchronized阻塞算法,J.U.C在性能上有了很大的提升。
CAS通過調用JNI的代碼實現的。JNI:Java Native Interface爲JAVA本地調用,允許java調用其他語言。而compareAndSwapInt就是藉助C來調用CPU底層指令實現的。
下面是sun.misc.Unsafe類的compareAndSwapInt()方法的源代碼:
public final native boolean compareAndSwapInt(Object o, long offset,int expected,int x)
1、當前的實例 2、實例變量的內存地址偏移量 3、預期的舊值 4、要更新的值
首先說明,處理器會自動保證基本的內存操作是原子性的。處理器保證從系統內存中讀取或寫入一個字節是原子的。意思是,當一個處理器讀取一個字節時,其他處理器不能訪問這個字節的內存地址。
當然, long和 double類型在32位操作系統中的讀寫操作不是原子的,因爲 long和 double佔64位,需要分成2個步驟來處理,在讀寫時分別拆成2個字節進行讀寫。因此 long和 double類型的數據在進行計算時需要注意這個問題。
總線鎖
第一個機制是通過總線鎖保證原子性。如果多個處理器同時對共享變量進行讀改寫(i++就是經典的讀改寫操作)操作,那麼共享變量就會被多個處理器同時進行操作,這樣讀改寫操作就不是原子的,操作完之後共享變量的值會和期望的不一致,舉個例子:如果i=1,我們進行兩次i++操作,我們期望的結果是3,但是有可能結果是2。如下圖
處理器使用總線鎖就是來解決這個問題的。所謂總線鎖就是使用處理器提供的一個LOCK#信號,當一個處理器在總線上輸出此信號時,其他處理器的請求將被阻塞住,那麼該處理器可以獨佔使用共享內存。
緩存鎖
第二個機制是通過緩存鎖定保證原子性。在同一時刻我們只需保證對某個內存地址的操作是原子性即可,但總線鎖定把CPU和內存之間通信鎖住了,這使得鎖定期間,其他處理器不能操作其他內存地址的數據,所以總線鎖定的開銷比較大,最近的處理器在某些場合下使用緩存鎖定代替總線鎖定來進行優化。
頻繁使用的內存會緩存在處理器的L1,L2和L3高速緩存裏,那麼原子操作就可以直接在處理器內部緩存中進行,並不需要聲明總線鎖,在奔騰6和最近的處理器中可以使用“緩存鎖定”的方式來實現複雜的原子性。所謂“緩存鎖定”就是如果緩存在處理器緩存行中內存區域在LOCK操作期間被鎖定,當它執行鎖操作回寫內存時,處理器不在總線上聲言LOCK#信號,而是修改內部的內存地址,並允許它的緩存一致性機制來保證操作的原子性,因爲緩存一致性機制會阻止同時修改被兩個以上處理器緩存的內存區域數據,當其他處理器回寫已被鎖定的緩存行的數據時會起緩存行無效,在例1中,當CPU1修改緩存行中的i時使用緩存鎖定,那麼CPU2就不能同時緩存了i的緩存行。
但是有兩種情況下處理器不會使用緩存鎖定。第一種情況是:當操作的數據不能被緩存在處理器內部,或操作的數據跨多個緩存行(cache line),則處理器會調用總線鎖定。第二種情況是:有些處理器不支持緩存鎖定。對於Inter486和奔騰處理器,就算鎖定的內存區域在處理器的緩存行中也會調用總線鎖定。
ABA問題
假設這樣一種場景,當第一個線程執行CAS(V,E,U)操作。在獲取到當前變量V,準備修改爲新值U前,另外兩個線程已連續修改了兩次變量V的值,使得該值又恢復爲舊值,這樣的話,我們就無法正確判斷這個變量是否已被修改過,如下圖:
這就是典型的CAS的ABA問題,一般情況這種情況發現的概率比較小,可能發生了也不會造成什麼問題,比如說我們對某個做加減法,不關心數字的過程,那麼發生ABA問題也沒啥關係。但是在某些情況下還是需要防止的,那麼該如何解決呢?在Java中解決ABA問題,我們可以使用以下原子類
AtomicStampedReference類
AtomicStampedReference原子類是一個帶有時間戳的對象引用,在每次修改後,AtomicStampedReference不僅會設置新值而且還會記錄更改的時間。當AtomicStampedReference設置對象值時,對象值以及時間戳都必須滿足期望值才能寫入成功,這也就解決了反覆讀寫時,無法預知值是否已被修改的窘境
底層實現爲: 通過Pair私有內部類存儲數據和時間戳, 並構造volatile修飾的私有實例。
用CAS實現volatile原子性方式
原子性表現爲每個可以單獨操作,不互相依賴,在線程中表現爲每個線程都有所以它自己的一份copy值,不定期的刷新到主內存。(如果有鎖,ulock時刷新到主內存)
而volatile變量不具有原子性,每次讀寫都是自己去主內存讀主內存的值,也真是由於此種原因不能進行計數器操作,例如:
volatile i =1;
線程A,線程B 同時 i++;
i++ 即
i=i; //從主內存中讀 1
i+1; //通過獲取的值。計算 2
i=i+1; //把計算的值寫入主內存中 3
當線程執行順序如下時 A1 – >B1—>A2—>A3—>A1—>B2—>B3, 最後結果導致運行了兩次結果還是2
對此,可以用CAS算法進行改進,CAS也可成爲樂觀鎖,實現原理,通過保存原有值進行比較結果,直到更改成功,即自旋volatile變量實現。
實現原理,CAS保存了3個值 H當前值(作爲預期值),V內存值,S計算值
代碼實現如下:
public final int incrementAndGet(h, s) {
for (;;) {
inth=i; //A線程叫AH,B線程描述爲BH 1
int s = i +1; // A線程叫AS,B線程描述爲BS 2
if(h==i){ // 比較內存值和預期值 3
i=s; // 如果相同,賦值,成功CAS 4
return s;
}
}
A1 (A開始時用AH保存內存中此時的i值)->
B1(B開始時也用BH保存當前i值)->
A2 (把計算值2賦給AS)
A3(比較保存的AH和讀取內存值Ai,都是等於1,未改變)
A4(所以CAS成功,把AS即2放入內存中)
B2(把計算值2賦給BS)
B3(比較BH和讀取當前內存值Bi,BH是1,Bi是2,所以不相等,返回到B1)
B1 (故重新取出內存值i,重複計算,此時BH=Bi=2,BS=3賦給主內存,完成計數)
CAS特點:
優點:
- 競爭不大的時候系統開銷小。
缺點:
-
循環時間長開銷大。
-
ABA問題。
-
只能保證一個共享變量的原子操作。
AQS
概述
java的內置鎖一直都是備受爭議的,在JDK 1.6之前,synchronized這個重量級鎖其性能一直都是較爲低下,雖然在1.6後,進行大量的鎖優化策略,但是與Lock相比synchronized還是存在一些缺陷的:雖然synchronized提供了便捷性的隱式獲取鎖釋放鎖機制(基於JVM機制),但是它卻缺少了獲取鎖與釋放鎖的可操作性,可中斷、超時獲取鎖,且它爲獨佔式在高併發場景下性能大打折扣。
在介紹Lock的時候,我們需要先熟悉一個非常重要的組件,掌握了該組件JUC包下面很多問題都不在是問題了。該組件就是AQS。
AQS:AbstractQueuedSynchronizer,即隊列同步器。它是構建鎖或者其他同步組件的基礎框架(如ReentrantLock、ReentrantReadWriteLock、Semaphore等),JUC併發包的作者(Doug Lea)期望它能夠成爲實現大部分同步需求的基礎。它是JUC併發包中的核心基礎組件。
AQS解決了子啊實現同步器時涉及當的大量細節問題,例如獲取同步狀態、FIFO同步隊列。基於AQS來構建同步器可以帶來很多好處。它不僅能夠極大地減少實現工作,而且也不必處理在多個位置上發生的競爭問題。
在基於AQS構建的同步器中,只能在一個時刻發生阻塞,從而降低上下文切換的開銷,提高了吞吐量。同時在設計AQS時充分考慮了可伸縮行,因此J.U.C中所有基於AQS構建的同步器均可以獲得這個優勢。
AQS的主要使用方式是繼承,子類通過繼承同步器並實現它的抽象方法來管理同步狀態。
它維護了一個volatile int state(代表共享資源)和一個FIFO線程等待隊列(多線程爭用資源被阻塞時會進入此隊列)。當state>0時表示已經獲取了鎖,當state = 0時表示釋放了鎖。這裏volatile是核心關鍵詞,具體volatile的語義,在此不述。state的訪問方式有三種:
- int getState():返回同步狀態的當前值。
- void setState(long newState):設置同步狀態的值。
- boolean compareAndSetState(int expect, int update):如果當前狀態值等於預期值,則以原子方式將同步狀態設置爲給定的更新值。
AQS通過內置的FIFO同步隊列來完成資源獲取線程的排隊工作,如果當前線程獲取同步狀態失敗(鎖)時,AQS則會將當前線程以及等待狀態等信息構造成一個節點(Node)並將其加入同步隊列,同時會阻塞當前線程,當同步狀態釋放時,則會把節點中的線程喚醒,使其再次嘗試獲取同步狀態。
基本方法
- getState():返回同步狀態的當前值;
- setState(int newState):設置當前同步狀態;
- compareAndSetState(int expect, int update):使用CAS設置當前狀態,該方法能夠保證狀態設置的原子性;
- tryAcquire(int arg):獨佔式獲取同步狀態,獲取同步狀態成功後,其他線程需要等待該線程釋放同步狀態才能獲取同步狀態
- tryRelease(int arg):獨佔式釋放同步狀態;
- tryAcquireShared(int arg):共享式獲取同步狀態,返回值大於等於0則表示獲取成功,否則獲取失敗;
- tryReleaseShared(int arg):共享式釋放同步狀態;
- isHeldExclusively():當前同步器是否在獨佔式模式下被線程佔用,一般該方法表示是否被當前線程所獨佔;
- acquire(int arg):獨佔式獲取同步狀態,如果當前線程獲取同步狀態成功,則由該方法返回,否則,將會進入同步隊列等待,該方法將會調用可重寫的tryAcquire(int arg)方法;
- acquireInterruptibly(int arg):與acquire(int arg)相同,但是該方法響應中斷,當前線程爲獲取到同步狀態而進入到同步隊列中,如果當前線程被中斷,則該方法會拋出InterruptedException異常並返回;
- tryAcquireNanos(int arg,long nanos):超時獲取同步狀態,如果當前線程在nanos時間內沒有獲取到同步狀態,那麼將會返回false,已經獲取則返回true;
- acquireShared(int arg):共享式獲取同步狀態,如果當前線程未獲取到同步狀態,將會進入同步隊列等待,與獨佔式的主要區別是在同一時刻可以有多個線程獲取到同步狀態;
- acquireSharedInterruptibly(int arg):共享式獲取同步狀態,響應中斷;
- tryAcquireSharedNanos(int arg, long nanosTimeout):共享式獲取同步狀態,增加超時限制;
- release(int arg):獨佔式釋放同步狀態,該方法會在釋放同步狀態之後,將同步隊列中第一個節點包含的線程喚醒;
- releaseShared(int arg):共享式釋放同步狀態;
獨佔鎖和非獨佔鎖
AQS定義兩種資源共享方式:Exclusive(獨佔,只有一個線程能執行,如ReentrantLock)和Share(共享,多個線程可同時執行,如Semaphore/CountDownLatch)。一般來說,自定義同步器要麼是獨佔方法,要麼是共享方式,他們也只需實現tryAcquire-tryRelease、tryAcquireShared-tryReleaseShared中的一種即可。但AQS也支持自定義同步器同時實現獨佔和共享兩種方式,如ReentrantReadWriteLock。
acquire(int):
此方法是獨佔模式下線程獲取共享資源的頂層入口。如果獲取到資源,線程直接返回,否則進入等待隊列,直到獲取到資源爲止,且整個過程忽略中斷的影響。這也正是lock()的語義,當然不僅僅只限於lock()。獲取到資源後,線程就可以去執行其臨界區代碼了。
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
流程:
- tryAcquire()嘗試直接去獲取資源,如果成功則直接返回;
- 如果失敗,addWaiter()將該線程加入等待隊列的尾部,並標記爲獨佔模式;
- acquireQueued()使線程在等待隊列中獲取資源,一直獲取到資源後才返回。如果在整個等待過程中被中斷過,則返回true,否則返回false。
- 如果線程在等待過程中被中斷過,它是不響應的。只是獲取資源後纔再進行自我中斷selfInterrupt(),將中斷補上。
tryAcquire()方法
此方法嘗試去獲取獨佔資源。如果獲取成功,則直接返回true,否則直接返回false。AQS這裏只定義了一個接口,具體資源的獲取交由自定義同步器去實現了(通過state的get/set/CAS)。
protected boolean tryAcquire(int arg) {
throw new UnsupportedOperationException();
}
這裏之所以沒有定義成abstract,是因爲獨佔模式下只用實現tryAcquire-tryRelease,而共享模式下只用實現tryAcquireShared-tryReleaseShared。如果都定義成abstract,那麼每個模式也要去實現另一模式下的接口。說到底,Doug Lea還是站在咱們開發者的角度,儘量減少不必要的工作量。
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
// 2. 當前尾節點是否爲null?
Node pred = tail;
if (pred != null) {
// 2.2 將當前節點尾插入的方式插入同步隊列中
node.prev = pred;
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
// 2.1. 當前同步隊列尾節點爲null,說明當前線程是第一個加入同步隊列進行等待的線程
enq(node);
return node;
}
程序的邏輯主要分爲兩個部分:
- 當前同步隊列的尾節點爲null,調用方法enq()插入;
- 當前隊列的尾節點不爲null,則採用尾插入(compareAndSetTail()方法)的方式入隊。
另外還會有另外一個問題:如果 if (compareAndSetTail(pred, node))
爲false怎麼辦?會繼續執行到enq()方法,同時很明顯compareAndSetTail是一個CAS操作,通常來說如果CAS操作失敗會繼續自旋(死循環)進行重試。
enq(final Node node)
private Node enq(final Node node) {
for (;;) {
Node t = tail;
if (t == null) { // Must initialize
//1. 構造頭結點
if (compareAndSetHead(new Node()))
tail = head;
} else {
// 2. 尾插入,CAS操作失敗自旋嘗試
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
對enq()方法可以做這樣的總結:
- 在當前線程是第一個加入同步隊列時,調用compareAndSetHead(new Node())方法,完成鏈式隊列的頭結點的初始化;
- 自旋不斷嘗試CAS尾插入節點直至成功爲止。
acquireQueued()
在同步隊列中的節點(線程)會做什麼事情了來保證自己能夠有機會獲得獨佔式鎖了?帶着這樣的問題我們就來看看acquireQueued()方法,從方法名就可以很清楚,這個方法的作用就是排隊獲取鎖的過程:
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
// 1. 獲得當前節點的先驅節點
final Node p = node.predecessor();
// 2. 當前節點能否獲取獨佔式鎖
// 2.1 如果當前節點的先驅節點是頭結點並且成功獲取同步狀態,即可以獲得獨佔式鎖
if (p == head && tryAcquire(arg)) {
//隊列頭指針用指向當前節點
setHead(node);
//釋放前驅節點
p.next = null; // help GC
failed = false;
return interrupted;
}
// 2.2 獲取鎖失敗,線程進入等待狀態等待獲取獨佔式鎖
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
程序邏輯通過註釋已經標出,整體來看這是一個這又是一個自旋的過程(for (;;)),代碼首先獲取當前節點的先驅節點,如果先驅節點是頭結點的並且成功獲得同步狀態的時候(if (p == head && tryAcquire(arg))),當前節點所指向的線程能夠獲取鎖。反之,獲取鎖失敗進入等待狀態。
release()方法
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
這段代碼邏輯就比較容易理解了,如果同步狀態釋放成功(tryRelease返回true)則會執行if塊中的代碼,當head指向的頭結點不爲null,並且該節點的狀態值不爲0的話纔會執行unparkSuccessor()方法。unparkSuccessor方法源碼:
private void unparkSuccessor(Node node) {
/*
* If status is negative (i.e., possibly needing signal) try
* to clear in anticipation of signalling. It is OK if this
* fails or if status is changed by waiting thread.
*/
int ws = node.waitStatus;
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
/*
* Thread to unpark is held in successor, which is normally
* just the next node. But if cancelled or apparently null,
* traverse backwards from tail to find the actual
* non-cancelled successor.
*/
//頭節點的後繼節點
Node s = node.next;
if (s == null || s.waitStatus > 0) {
s = null;
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)
s = t;
}
if (s != null)
//後繼節點不爲null時喚醒該線程
LockSupport.unpark(s.thread);
}
首先獲取頭節點的後繼節點,當後繼節點的時候會調用LookSupport.unpark()方法,該方法會喚醒該節點的後繼節點所包裝的線程。因此,每一次鎖釋放後就會喚醒隊列中該節點的後繼節點所引用的線程,從而進一步可以佐證獲得鎖的過程是一個FIFO(先進先出)的過程。
acquireInterruptibly()方法
我們知道lock相較於synchronized有一些更方便的特性,比如能響應中斷以及超時等待等特性,現在我們依舊採用通過學習源碼的方式來看看能夠響應中斷是怎麼實現的。可響應中斷式鎖可調用方法lock.lockInterruptibly();而該方法其底層會調用AQS的acquireInterruptibly方法
public final void acquireInterruptibly(int arg)
throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
if (!tryAcquire(arg))
//線程獲取鎖失敗
doAcquireInterruptibly(arg);
}
private void doAcquireInterruptibly(int arg)
throws InterruptedException {
//將節點插入到同步隊列中
final Node node = addWaiter(Node.EXCLUSIVE);
boolean failed = true;
try {
for (;;) {
final Node p = node.predecessor();
//獲取鎖出隊
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return;
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
//線程中斷拋異常
throw new InterruptedException();
}
} finally {
if (failed)
cancelAcquire(node);
}
}
與acquire方法邏輯幾乎一致,唯一的區別是當parkAndCheckInterrupt返回true時即線程阻塞時該線程被中斷,代碼拋出被中斷異常。
tryAcquireNanos()方法(超時等待獲取鎖)
通過調用lock.tryLock(timeout,TimeUnit)方式達到超時等待獲取鎖的效果,該方法會在三種情況下才會返回:
- 在超時時間內,當前線程成功獲取了鎖;
- 當前線程在超時時間內被中斷;
- 超時時間結束,仍未獲得鎖返回false。
public final boolean tryAcquireNanos(int arg, long nanosTimeout)
throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
return tryAcquire(arg) ||
//實現超時等待的效果
doAcquireNanos(arg, nanosTimeout);
}
private boolean doAcquireNanos(int arg, long nanosTimeout)
throws InterruptedException {
if (nanosTimeout <= 0L)
return false;
//1. 根據超時時間和當前時間計算出截止時間
final long deadline = System.nanoTime() + nanosTimeout;
final Node node = addWaiter(Node.EXCLUSIVE);
boolean failed = true;
try {
for (;;) {
final Node p = node.predecessor();
//2. 當前線程獲得鎖出隊列
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return true;
}
// 3.1 重新計算超時時間
nanosTimeout = deadline - System.nanoTime();
// 3.2 已經超時返回false
if (nanosTimeout <= 0L)
return false;
// 3.3 線程阻塞等待
if (shouldParkAfterFailedAcquire(p, node) &&
nanosTimeout > spinForTimeoutThreshold)
LockSupport.parkNanos(this, nanosTimeout);
// 3.4 線程被中斷拋出被中斷異常
if (Thread.interrupted())
throw new InterruptedException();
}
} finally {
if (failed)
cancelAcquire(node);
}
}
顯然這段源碼最終是靠doAcquireNanos方法實現超時等待的效果。
程序邏輯同獨佔鎖可響應中斷式獲取基本一致,唯一的不同在於獲取鎖失敗後,對超時時間的處理上,在第1步會先計算出按照現在時間和超時時間計算出理論上的截止時間,比如當前時間是8h10min,超時時間是10min,那麼根據deadline = System.nanoTime() + nanosTimeout
計算出剛好達到超時時間時的系統時間就是8h 10min+10min = 8h 20min。然後根據deadline - System.nanoTime()
就可以判斷是否已經超時了,比如,當前系統時間是8h 30min很明顯已經超過了理論上的系統時間8h 20min,deadline - System.nanoTime()
計算出來就是一個負數,自然而然會在3.2步中的If判斷之間返回false。如果還沒有超時即3.2步中的if判斷爲true時就會繼續執行3.3步通過LockSupport.parkNanos使得當前線程阻塞,同時在3.4步增加了對中斷的檢測,若檢測出被中斷直接拋出被中斷異常。流程如下圖:
acquireShared()方法(共享鎖的獲取)
共享鎖的獲取如下:
public final void acquireShared(int arg) {
if (tryAcquireShared(arg) < 0)
doAcquireShared(arg);
}
這段源碼的邏輯很容易理解,在該方法中會首先調用tryAcquireShared方法,tryAcquireShared返回值是一個int類型,當返回值爲大於等於0的時候方法結束說明獲得成功獲取鎖,否則,表明獲取同步狀態失敗即所引用的線程獲取鎖失敗,會執行doAcquireShared方法
private void doAcquireShared(int arg) {
final Node node = addWaiter(Node.SHARED);
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor();
if (p == head) {
int r = tryAcquireShared(arg);
if (r >= 0) {
// 當該節點的前驅節點是頭結點且成功獲取同步狀態
setHeadAndPropagate(node, r);
p.next = null; // help GC
if (interrupted)
selfInterrupt();
failed = false;
return;
}
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
邏輯幾乎和獨佔式鎖的獲取一模一樣,這裏的自旋過程中能夠退出的條件是當前節點的前驅節點是頭結點並且tryAcquireShared(arg)返回值大於等於0即能成功獲得同步狀態。
releaseShared()
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
doReleaseShared();
return true;
}
return false;
}
private void doReleaseShared() {
/*
* Ensure that a release propagates, even if there are other
* in-progress acquires/releases. This proceeds in the usual
* way of trying to unparkSuccessor of head if it needs
* signal. But if it does not, status is set to PROPAGATE to
* ensure that upon release, propagation continues.
* Additionally, we must loop in case a new node is added
* while we are doing this. Also, unlike other uses of
* unparkSuccessor, we need to know if CAS to reset status
* fails, if so rechecking.
*/
for (;;) {
Node h = head;
if (h != null && h != tail) {
int ws = h.waitStatus;
if (ws == Node.SIGNAL) {
if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
continue; // loop to recheck cases
unparkSuccessor(h);
}
else if (ws == 0 &&
!compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
continue; // loop on failed CAS
}
if (h == head) // loop if head changed
break;
}
}
這段方法跟獨佔式鎖釋放過程有點點不同,在共享式鎖的釋放過程中,對於能夠支持多個線程同時訪問的併發組件,必須保證多個線程能夠安全的釋放同步狀態,這裏採用的CAS保證,當CAS操作失敗continue,在下一次循環中進行重試。
可中斷(acquireSharedInterruptibly()方法),超時等待(tryAcquireSharedNanos()方法)
中斷鎖以及超時等待的特性其實現和獨佔式鎖可中斷獲取鎖以及超時等待的實現幾乎一致,具體的就不再說了。
ReentrantLock
公平鎖和非公平鎖
非公平鎖和公平鎖的區別是在tryAccquire時,是否判斷當前節點是頭節點,如果是非公平鎖不判斷,直接CAS,先搶到先得。
如果是公平鎖,必須是頭節點才能CAS。
如何創建:
//默認非公平鎖
Lock nonFairLock = new ReentrantLock();
nonFairLock.lock();
//創建公平鎖
Lock fairLock = new ReentrantLock(true);
fairLock.lock();
Synchronized 和 ReenTrantLock 的對比
① 兩者都是可重入鎖
兩者都是可重入鎖。“可重入鎖”概念是:自己可以再次獲取自己的內部鎖。比如一個線程獲得了某個對象的鎖,此時這個對象鎖還沒有釋放,當其再次想要獲取這個對象的鎖的時候還是可以獲取的,如果不可鎖重入的話,就會造成死鎖。同一個線程每次獲取鎖,鎖的計數器都自增1,所以要等到鎖的計數器下降爲0時才能釋放鎖。
② synchronized 依賴於 JVM 而 ReenTrantLock 依賴於 API
synchronized 是依賴於 JVM 實現的,前面我們也講到了 虛擬機團隊在 JDK1.6 爲 synchronized 關鍵字進行了很多優化,但是這些優化都是在虛擬機層面實現的,並沒有直接暴露給我們。ReenTrantLock 是 JDK 層面實現的(也就是 API 層面,需要 lock() 和 unlock 方法配合 try/finally 語句塊來完成),所以我們可以通過查看它的源代碼,來看它是如何實現的。
③ ReenTrantLock 比 synchronized 增加了一些高級功能
相比synchronized,ReenTrantLock增加了一些高級功能。主要來說主要有三點:①等待可中斷;②可實現公平鎖;③可實現選擇性通知(鎖可以綁定多個條件)
- ReenTrantLock提供了一種能夠中斷等待鎖的線程的機制,通過lock.lockInterruptibly()來實現這個機制。也就是說正在等待的線程可以選擇放棄等待,改爲處理其他事情。
- ReenTrantLock可以指定是公平鎖還是非公平鎖。而synchronized只能是非公平鎖。所謂的公平鎖就是先等待的線程先獲得鎖。 ReenTrantLock默認情況是非公平的,可以通過 ReenTrantLock類的
ReentrantLock(boolean fair)
構造方法來制定是否是公平的。 - synchronized關鍵字與wait()和notify/notifyAll()方法相結合可以實現等待/通知機制,ReentrantLock類當然也可以實現,但是需要藉助於Condition接口與newCondition() 方法。Condition是JDK1.5之後纔有的,它具有很好的靈活性,比如可以實現多路通知功能也就是在一個Lock對象中可以創建多個Condition實例(即對象監視器),線程對象可以註冊在指定的Condition中,從而可以有選擇性的進行線程通知,在調度線程上更加靈活。 在使用notify/notifyAll()方法進行通知時,被通知的線程是由 JVM 選擇的,用ReentrantLock類結合Condition實例可以實現“選擇性通知” ,這個功能非常重要,而且是Condition接口默認提供的。而synchronized關鍵字就相當於整個Lock對象中只有一個Condition實例,所有的線程都註冊在它一個身上。如果執行notifyAll()方法的話就會通知所有處於等待狀態的線程這樣會造成很大的效率問題,而Condition實例的signalAll()方法 只會喚醒註冊在該Condition實例中的所有等待線程。
如果你想使用上述功能,那麼選擇ReenTrantLock是一個不錯的選擇。
④ 性能已不是選擇標準
在JDK1.6之前,synchronized 的性能是比 ReenTrantLock 差很多。具體表示爲:synchronized 關鍵字吞吐量歲線程數的增加,下降得非常嚴重。而ReenTrantLock 基本保持一個比較穩定的水平。我覺得這也側面反映了, synchronized 關鍵字還有非常大的優化餘地。後續的技術發展也證明了這一點,我們上面也講了在 JDK1.6 之後 JVM 團隊對 synchronized 關鍵字做了很多優化。JDK1.6 之後,synchronized 和 ReenTrantLock 的性能基本是持平了。所以網上那些說因爲性能才選擇 ReenTrantLock 的文章都是錯的!JDK1.6之後,性能已經不是選擇synchronized和ReenTrantLock的影響因素了!而且虛擬機在未來的性能改進中會更偏向於原生的synchronized,所以還是提倡在synchronized能滿足你的需求的情況下,優先考慮使用synchronized關鍵字來進行同步!優化後的synchronized和ReenTrantLock一樣,在很多地方都是用到了CAS操作。
ReentrantReadWriteLock
ReentrantLock(排他鎖)具有完全互斥排他的效果,即同一時刻只允許一個線程訪問,這樣做雖然雖然保證了實例變量的線程安全性,但效率非常低下。ReadWriteLock接口的實現類-ReentrantReadWriteLock讀寫鎖就是爲了解決這個問題。
讀寫鎖維護了兩個鎖,一個是讀操作相關的鎖也成爲共享鎖,一個是寫操作相關的鎖 也稱爲排他鎖。通過分離讀鎖和寫鎖,其併發性比一般排他鎖有了很大提升。
多個讀鎖之間不互斥,讀鎖與寫鎖互斥,寫鎖與寫鎖互斥(只要出現寫操作的過程就是互斥的。)。
ReentrantReadWriteLock具有如下特性:
一、鎖的獲取順序:
- 非公平模式(默認):當使用此種模式的時候,將不會指定進入讀寫鎖的順序,即一個線程獲取到鎖並釋放後,可能立即再次獲取鎖,也可能導致一個線程可能一直嘗試搶鎖,但是獲取不到,其吞吐量通常要高於公平鎖。
- 公平模式:當使用此種模式的時候,線程利用一個近似到達順序的策略來爭奪進入,等待時間最長的線程將最先獲取到鎖,該種模式會保證獲取鎖的時間順序,但是吞吐量會有所犧牲。
二、可重入性:當持有讀鎖的線程獲取後能再次獲取同一把鎖,寫鎖獲取之後能夠再次獲取寫鎖,同時也能夠獲取讀鎖,但是反之則不可以,即持有讀鎖的線程,不可以再次獲取寫鎖。
三、鎖降級:遵循獲取寫鎖,獲取讀鎖再釋放寫鎖的次序,寫鎖能夠降級成爲讀鎖,但是反之不可以,即讀鎖不可以升級爲寫鎖。
四、重入數:讀取鎖和寫入鎖的數量最大分別只能是65535(包括重入數)。
CONDITION
synchronized關鍵字與wait()和notify/notifyAll()方法相結合可以實現等待/通知機制,ReentrantLock類當然也可以實現,但是需要藉助於Condition接口與newCondition() 方法。Condition是JDK1.5之後纔有的,它具有很好的靈活性,比如可以實現多路通知功能也就是在一個Lock對象中可以創建多個Condition實例(即對象監視器),線程對象可以註冊在指定的Condition中,從而可以有選擇性的進行線程通知,在調度線程上更加靈活。
condition中實現的方法:
單個等待通知機制:
public class UseSingleConditionWaitNotify {
public static void main(String[] args) throws InterruptedException {
MyService service = new MyService();
ThreadA a = new ThreadA(service);
a.start();
Thread.sleep(3000);
service.signal();
}
static public class MyService {
private Lock lock = new ReentrantLock();
public Condition condition = lock.newCondition();
public void await() {
lock.lock();
try {
System.out.println(" await時間爲" + System.currentTimeMillis());
condition.await();
System.out.println("這是condition.await()方法之後的語句,condition.signal()方法之後我才被執行");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void signal() throws InterruptedException {
lock.lock();
try {
System.out.println("signal時間爲" + System.currentTimeMillis());
condition.signal();
Thread.sleep(3000);
System.out.println("這是condition.signal()方法之後的語句");
} finally {
lock.unlock();
}
}
}
static public class ThreadA extends Thread {
private MyService service;
public ThreadA(MyService service) {
super();
this.service = service;
}
@Override
public void run() {
service.await();
}
}
}
在使用wait/notify實現等待通知機制的時候我們知道必須執行完notify()方法所在的synchronized代碼塊後才釋放鎖。在這裏也差不多,必須執行完signal所在的try語句塊之後才釋放鎖,condition.await()後的語句才能被執行。
使用Condition實現順序執行:
public class ConditionSeqExec {
volatile private static int nextPrintWho = 1;
private static ReentrantLock lock = new ReentrantLock();
final private static Condition conditionA = lock.newCondition();
final private static Condition conditionB = lock.newCondition();
final private static Condition conditionC = lock.newCondition();
public static void main(String[] args) {
Thread threadA = new Thread() {
public void run() {
try {
lock.lock();
while (nextPrintWho != 1) {
conditionA.await();
}
for (int i = 0; i < 3; i++) {
System.out.println("ThreadA " + (i + 1));
}
nextPrintWho = 2;
//通知conditionB實例的線程運行
conditionB.signalAll();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
};
Thread threadB = new Thread() {
public void run() {
try {
lock.lock();
while (nextPrintWho != 2) {
conditionB.await();
}
for (int i = 0; i < 3; i++) {
System.out.println("ThreadB " + (i + 1));
}
nextPrintWho = 3;
//通知conditionC實例的線程運行
conditionC.signalAll();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
};
Thread threadC = new Thread() {
public void run() {
try {
lock.lock();
while (nextPrintWho != 3) {
conditionC.await();
}
for (int i = 0; i < 3; i++) {
System.out.println("ThreadC " + (i + 1));
}
nextPrintWho = 1;
//通知conditionA實例的線程運行
conditionA.signalAll();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
};
Thread[] aArray = new Thread[5];
Thread[] bArray = new Thread[5];
Thread[] cArray = new Thread[5];
for (int i = 0; i < 5; i++) {
aArray[i] = new Thread(threadA);
bArray[i] = new Thread(threadB);
cArray[i] = new Thread(threadC);
aArray[i].start();
bArray[i].start();
cArray[i].start();
}
}
}
CyclicBarrier
CyclicBarrier是一個同步輔助類,它允許一組線程互相等待,直到到達某個公共屏障點。在涉及一組固定大小的線程的程序中,這些線程必須不時地互相等待,此時CyclicBarrier很有用。它的功能與Thread中的join()非常的相似,不過它的功能會更加的強大。
CyclicBarrier的實現機制是依賴於ReentrantLock於Condition實現的,CyclicBarrier構造方法如下:
CyclicBarrier(int parties)
創建一個新的 CyclicBarrier,它將在給定數量的參與者(線程)處於等待狀態時啓動,但它不會在啓動 barrier 時執行預定義的操作。
------------------------------------------------------------------------------
CyclicBarrier(int parties, Runnable barrierAction)
創建一個新的 CyclicBarrier,它將在給定數量的參與者(線程)處於等待狀態時啓動,並在啓動 barrier 時執行給定的屏障操作,該操作由最後一個進入 barrier 的線程執行。
類結構如下:
方法包括:
int await()
在所有參與者都已經在此 barrier 上調用 await 方法之前,將一直等待。
------------------------------------------------------------------------------
int await(long timeout, TimeUnit unit)
在所有參與者都已經在此屏障上調用 await 方法之前將一直等待,或者超出了指定的等待時間。
------------------------------------------------------------------------------
int getNumberWaiting()
返回當前在屏障處等待的參與者數目。
------------------------------------------------------------------------------
int getParties()
返回要求啓動此 barrier 的參與者數目。
------------------------------------------------------------------------------
boolean isBroken()
查詢此屏障是否處於損壞狀態。
------------------------------------------------------------------------------
void reset()
將屏障重置爲其初始狀態。
示例如下:
public class CyclicBarrierDemo {
public static void main(String[] args) throws Exception {
CyclicBarrier cyclicBarrier = new CyclicBarrier(5);
for (int i = 0; i < 5; i++) {
new Thread(() -> {
try {
System.out.println("當前線程:" + Thread.currentThread().getName() + ", 等待其他線程準備就緒");
cyclicBarrier.await();
} catch (Exception e) {
e.printStackTrace();
}
}).start();
}
System.out.println("全部線程就緒,開始執行");
}
}
源碼分析
構造方法:
public CyclicBarrier(int parties, Runnable barrierAction) {
//線程數目小與等於0,拋出異常
if (parties <= 0) {
throw new IllegalArgumentException();
}
this.parties = parties;
this.count = parties;
this.barrierCommand = barrierAction;
}
可以指定關聯該CyclicBarrier的線程數量,並且可以指定在所有線程都進入屏障後的執行動作,該執行動作由最後一個進行屏障的線程執行。
如果不指定Runnable對象,即不進行任何操作。(默認構造方法barrierAction傳null)
await()方法:
ublic int await() throws InterruptedException, BrokenBarrierException {
try {
return dowait(false, 0L);
} catch (TimeoutException toe) {
throw new Error(toe); // cannot happen
}
}
/**
* Main barrier code, covering the various policies.
*/
private int dowait(boolean timed, long nanos)
throws InterruptedException, BrokenBarrierException,
TimeoutException {
final ReentrantLock lock = this.lock;
//1、獲取鎖
lock.lock();
try {
//2、保存當前代
final Generation g = generation;
if (g.broken)
throw new BrokenBarrierException();
if (Thread.interrupted()) {
breakBarrier();
throw new InterruptedException();
}
//3、計數器自減
int index = --count;
//4、當計數器爲0時,結束流程
if (index == 0) { // tripped
boolean ranAction = false;
try {
//5、獲取結束時執行動作
final Runnable command = barrierCommand;
//如果動作不爲空,執行
if (command != null)
command.run();
ranAction = true;
//6、重置當前代
nextGeneration();
return 0;
} finally {
//7、未執行任何動作,破壞掉柵欄
if (!ranAction)
breakBarrier();
}
}
// loop until tripped, broken, interrupted, or timed out
// 進行死循環,直到被破壞、打斷、超時,結束循環
for (;;) {
try {
//8、如果未設置超時,當前線程進入Condition的等待隊列
if (!timed)
trip.await();
//如果設置了超時時間,當前線程在超時時間之前,進入等待隊列等待
else if (nanos > 0L)
nanos = trip.awaitNanos(nanos);
} catch (InterruptedException ie) {
//9、如果出現打斷異常,判斷保存的代等於當前代並且屏障沒有被損壞
if (g == generation && ! g.broken) {
//10、破壞掉柵欄
breakBarrier();
throw ie;
} else {
// We're about to finish waiting even if we had not
// been interrupted, so this interrupt is deemed to
// "belong" to subsequent execution.
Thread.currentThread().interrupt();
}
}
//11、如果保存的代被破壞,拋出異常
if (g.broken)
throw new BrokenBarrierException();
//12、如果保存的代不等於當前代,返回index
if (g != generation)
return index;
//13、如果設置了等待時間,並且等待時間小於0,破壞柵欄,並拋出異常
if (timed && nanos <= 0L) {
breakBarrier();
throw new TimeoutException();
}
}
} finally {
//14、釋放鎖資源
lock.unlock();
}
}
nextGeneration()爲釋放需要等待的所有隊列,並重置當前代。代的概念這裏我們說一下,它是CyclicBarrier中的一個內部類,它的作用更像是一個標誌位的作用,其只有一個屬性,broken,記錄當前代釋是否被破壞。nextGeneration()會在全部線程進入屏障後會被調用,即生成下一個代,使得全部線程又可以重新進入到柵欄中,從這裏可以得知,CyclicBarrier的柵欄是可以多次複用的,而這個特性與另一個功能相似的類CountDownLatch有所不同。
private void nextGeneration() {
// signal completion of last generation
trip.signalAll();
// set up next generation
count = parties;
generation = new Generation();
}
breakBarrier()破壞當前代,內部方法比較簡單,將當前代的破壞狀態設置爲true,並喚醒在當前Condition對象的等待隊列中等待的全部線程。
private void breakBarrier() {
generation.broken = true;
count = parties;
trip.signalAll();
}
整個流程如下:
- 調用await方法
- 獲取鎖資源,加鎖,判斷當前代是否被破壞,如果被破壞,拋出BrokenBarrierException
- 判斷線程是否被打斷,如果被打斷,broken置爲true,喚醒所有等待線程,拋出InterruptedException
- 計數器自減,重置當前代,返回0
- 如果計數器不爲0,循環線程進入當前condition的等待隊列,並掛起,等待喚醒
- 中斷、破壞和超時會跳出循環
- 被喚醒後,如果保存的代和當前代不等,返回當前計數
- 釋放鎖
使用場景:CyclicBarrier可以用於多個線程執行任務,需要等待多個線程全部執行完畢後,纔可以輸出最終結果,其在多線程開發場景下,非常的常用。
CountDownLatch
CountDownLatch是一個同步輔助類,與CyclicBarrier功能相似,它允許一組線程互相等待,直到到達某個公共屏障點。
但是它與CyclicBarrier不同,具體表現:
- CountDownLatch一般用於某個線程A等待若干個其他線程執行完任務之後,它才執行;
- 而CyclicBarrier一般用於一組線程互相等待至某個狀態,然後這一組線程再同時執行;
- CountDownLatch是不能夠重用的,而CyclicBarrier是可以重用的。
CountDownLatch使用示例:
public class CountDownLatchDemo {
public static void main(String[] args) throws Exception {
CountDownLatch countDownLatch = new CountDownLatch(5);
for (int i = 0; i < 5; i++) {
new Thread(() -> {
System.out.println("等待其他線程執行開始");
countDownLatch.countDown();
}).start();
}
countDownLatch.await();
System.out.println("全部線程執行完畢");
}
}
CountDownLatch源碼實現
CountDownLatch的實現是基於AQS的同步隊列,通過重寫AQS的抽象方法,同時採用共享鎖的獲取和釋放方式。
代碼如下:
public class CountDownLatch {
private static final class Sync extends AbstractQueuedSynchronizer {
private static final long serialVersionUID = 4982264981922014374L;
Sync(int count) {
setState(count);
}
int getCount() {
return getState();
}
protected int tryAcquireShared(int acquires) {
return (getState() == 0) ? 1 : -1;
}
protected boolean tryReleaseShared(int releases) {
// Decrement count; signal when transition to zero
for (;;) {
int c = getState();
if (c == 0)
return false;
int nextc = c-1;
if (compareAndSetState(c, nextc))
return nextc == 0;
}
}
}
private final Sync sync;
public CountDownLatch(int count) {
if (count < 0) throw new IllegalArgumentException("count < 0");
this.sync = new Sync(count);
}
public void await() throws InterruptedException {
sync.acquireSharedInterruptibly(1);
}
public boolean await(long timeout, TimeUnit unit)
throws InterruptedException {
return sync.tryAcquireSharedNanos(1, unit.toNanos(timeout));
}
public void countDown() {
sync.releaseShared(1);
}
public long getCount() {
return sync.getCount();
}
public String toString() {
return super.toString() + "[Count = " + sync.getCount() + "]";
}
}
CountDownLatch的構造函數是構造一個用給定計數初始化的CountDownLatch,並且構造函數內完成了sync的初始化,並設置了AQS的state值。
Sync繼承於AQS ,重寫了其tryAcquireShared、tryReleaseShared這兩個方法。
await()方法:
await()的實現是基於AQS的,調用了其acquireSharedInterruptibly()方法,是AQS的共享式獲取同步狀態,首先判斷了線程是否被中斷,如果是,拋出異常,否則,調用子類的模板方法的實現。
public void await() throws InterruptedException {
sync.acquireSharedInterruptibly(1);
}
public final void acquireSharedInterruptibly(int arg)
throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
//調用模板方法子類的實現,即CountdownLatch的實現
if (tryAcquireShared(arg) < 0)
doAcquireSharedInterruptibly(arg);
}
protected int tryAcquireShared(int acquires) {
return (getState() == 0) ? 1 : -1;
}
doAcquireSharedInterruptibly()方法:
private void doAcquireSharedInterruptibly(int arg)
throws InterruptedException {
final Node node = addWaiter(Node.SHARED);
boolean failed = true;
try {
for (;;) {
final Node p = node.predecessor();
if (p == head) {
int r = tryAcquireShared(arg);
if (r >= 0) {
setHeadAndPropagate(node, r);
p.next = null; // help GC
failed = false;
return;
}
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt()) //掛起當前線程
throw new InterruptedException();
}
} finally {
if (failed)
cancelAcquire(node);
}
}
private Node addWaiter(Node mode) {
Node node = new Node(Thread.currentThread(), mode);
// Try the fast path of enq; backup to full enq on failure
Node pred = tail;
if (pred != null) {
node.prev = pred;
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
enq(node);
return node;
}
- 增加一個共享節點到同步隊列尾部,如果當前隊列中沒有節點,創建一個假的頭結點,將新的節點的前驅節點指向該假頭結點
- 進入自旋,獲取當前節點的前驅節點
- 如果前驅節點是頭結點,調用模板方法的實現獲取同步狀態
- 如果結果大於0,設置新的頭結點,並釋放掉當前節點,並跳出循環
- 否則,將當前線程掛起
- 過程中如果被打斷,會拋出中斷異常
countDown()方法:
public void countDown() {
sync.releaseShared(1);
}
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
doReleaseShared();
return true;
}
return false;
}
protected boolean tryReleaseShared(int releases) {
// Decrement count; signal when transition to zero
for (;;) {
// 獲取狀態
int c = getState();
// 如果狀態值爲0
if (c == 0)
return false;
//否則,將狀態值,減一
int nextc = c-1;
//CAS賦值
if (compareAndSetState(c, nextc))
return nextc == 0;
}
}
countDown()方法同樣是基於AQS的方法進行實現,調用其releaseShared(),共享式獲取同步狀態方法,從tryReleaseShared方法我們可以看到,每次一個線程調用其countdown的時候,都會對state進行減一操作,直到state爲0的時候,該方法返回true。
await具體流程:
- 線程執行await方法,進入同步隊列;
- 新增節點,判斷頭節點是否爲null,沒有創建假的頭節點(第一次)
- 加入同步隊列尾部
- 自旋判斷:前驅節點是否是頭節點,如果是,釋放。設置新的頭節點,如果不是,當前節點線程掛起,等待喚醒;
cowndown方法流程:
- 調用countdown方法,線程調用tryReleaseShared()方法時,會將當前同步狀態減一,當state爲0的時候,調用doReleaseShared();
- 調用doReleaseShared做了一件事,喚醒頭結點的線程;
- 頭結點線程(第一個節點的頭結點是假節點,沒有持有線程,會喚醒其下一個真實節點的線程)被喚醒後,拿到同步資源,退出循環,並喚醒下一個節點的線程,依次類推,直到喚醒全部同步隊列的線程.
Semaphore
Semaphore是一個計數信號量。從概念上講,信號量維護了一個許可集。如有必要,在許可可用前會阻塞每一個 acquire(),然後再獲取該許可。每個 release() 添加一個許可,從而可能釋放一個正在阻塞的獲取者。
Semaphore可以理解爲一個流量控制器,只允許指定數目的線程拿到許可,繼續執行,當有其他線程再希望拿到許可時,需要阻塞等待,直到拿到許可的線程執行完畢後,釋放許可,其他線程纔可以獲取到許可,進行執行。
Semaphore 有兩種模式,即公平模式與非公平模式,可以通過構造方法進行指定。
構造方法:
Semaphore(int permits)
創建具有給定的許可數和非公平的公平設置的 Semaphore。
---------------------------------------------------------------------
Semaphore(int permits, boolean fair)
創建具有給定的許可數和給定的公平設置的 Semaphore。
在非公平模式下,不對線程獲取許可的順序做任何保證,即一個線程可能剛剛獲取完許可並釋放許可,可以立刻再次獲取許可。
在公平模式下,對於任何調用獲取方法的線程而言,都按照處理它們調用這些方法的順序(即先進先出;FIFO)來選擇線程、獲得許可。
幾個重要的方法:
void acquire()
從此信號量獲取一個許可,在提供一個許可前一直將線程阻塞,否則線程被中斷。
void release()
釋放一個許可,將其返回給信號量。
void acquire(int permits)
從此信號量獲取給定數目的許可,在提供這些許可前一直將線程阻塞,或者線程已被中斷。
void release(int permits)
釋放給定數目的許可,將其返回到信號量。
示例如下:
public class SemaphoreDemo {
public static void main(String[] args) {
Semaphore semaphore = new Semaphore(1);
ExecutorService executors = Executors.newFixedThreadPool(3);
for (int i = 0; i < 3; i++) {
executors.execute(() -> {
System.out.println("線程池A,啓動線程,當前線程名稱:" + Thread.currentThread().getName());
try {
System.out.println("線程池A,當前線程名稱:" + Thread.currentThread().getName() + ",準備嘗試獲取許可");
semaphore.acquire();
System.out.println("線程池A,當前線程名稱:" + Thread.currentThread().getName() + ",獲取許可成功");
Thread.sleep(5 * 1000);
} catch (Exception e) {
e.printStackTrace();
}
semaphore.release();
System.out.println("線程池A,當前線程名稱:" + Thread.currentThread().getName() + "釋放許可成功");
});
}
}
}
輸出結果:
線程池A,啓動線程,當前線程名稱:pool-1-thread-2
線程池A,啓動線程,當前線程名稱:pool-1-thread-1
線程池A,當前線程名稱:pool-1-thread-1,準備嘗試獲取許可
線程池A,啓動線程,當前線程名稱:pool-1-thread-3
線程池A,當前線程名稱:pool-1-thread-1,獲取許可成功
線程池A,當前線程名稱:pool-1-thread-2,準備嘗試獲取許可
線程池A,當前線程名稱:pool-1-thread-3,準備嘗試獲取許可
線程池A,當前線程名稱:pool-1-thread-1釋放許可成功
線程池A,當前線程名稱:pool-1-thread-2,獲取許可成功
線程池A,當前線程名稱:pool-1-thread-2釋放許可成功
線程池A,當前線程名稱:pool-1-thread-3,獲取許可成功
線程池A,當前線程名稱:pool-1-thread-3釋放許可成功
Semaphore的實現基本與另外幾個併發輔助工具類差不多,內部實現一個同步器,去繼承AQS,重寫其模板方法,實現自己的功能。Semaphore支持兩種模式,一種是公平模式,一種是非公平模式,這部分的實現也是基於AQS。具體源碼分析略。
Semaphore、CountDownLatch、CyclicBarrier
Semaphore、CountDownLatch、CyclicBarrier這三個類都是用於實現併發輔助的工具類,但是它們在使用場景上有略微的區別,這裏我們橫向對比一下:
CountDownLatch和CyclicBarrier都能夠實現線程之間的等待,只不過它們側重點不同:
CountDownLatch一般用於某個線程A等待若干個其他線程執行完任務之後,它才執行;
而CyclicBarrier一般用於一組線程互相等待至某個狀態,然後這一組線程再同時執行;
另外,CountDownLatch是不能夠重用的,而CyclicBarrier是可以重用的。
Semaphore其實和鎖有點類似,它一般用於控制對某組資源的訪問權限,而鎖是控制對某個資源的訪問權限。
參考文檔:
https://www.cnblogs.com/romanjoy/p/8427960.html
https://blog.csdn.net/wtopps/article/details/82054186
https://blog.csdn.net/zhangdong2012/article/details/79983404
https://blog.csdn.net/m0_37822939/article/details/80040589
https://juejin.im/post/5aeb07ab6fb9a07ac36350c8#heading-6