筆記主要參考《Java併發編程的藝術》並且基於JDK1.8的源碼進行的刨析,此篇只分析獨占模式,後續在ReentrantReadWriteLock和 CountDownLatch中 會重點分析AQS的共用模式 一丶Lock 鎖是用來控制多個線程訪問共用資源的方式,一般來說,一個鎖可以防止多個線程同時 ...
筆記主要參考《Java併發編程的藝術》並且基於JDK1.8的源碼進行的刨析,此篇只分析獨占模式,後續在ReentrantReadWriteLock和 CountDownLatch中 會重點分析AQS的共用模式
一丶Lock
鎖是用來控制多個線程訪問共用資源的方式,一般來說,一個鎖可以防止多個線程同時訪問共用資源(這種鎖稱為獨占鎖,排他鎖)但是有些鎖可以允許多個線程併發訪問共用資源,比如讀寫鎖
1.Lock介面的方法:
方法 | 作用 |
---|---|
void lock() | 獲取鎖,調用該方法的線程將會獲取鎖,當鎖獲得之後從該方法返回 |
void lockInterruptibly() | 可中斷地獲取鎖,該方法會響應中斷,在鎖的獲取可以中斷當前線程,如果在獲取鎖之前設置了中斷標誌,or獲取鎖的中途被中斷or其他線程中斷該線程則拋出InterruptedException並清除當前線程的中斷狀 |
boolean tryLock() | 嘗試非阻塞的獲取鎖,調用方法會立即返回,如果可以獲取到鎖返回true |
boolean tryLock(long time, TimeUnit unit) throws InterruptedException | 超時獲取鎖,從當前返回有三種情況 1.超時時間內獲取到鎖 2.當前線程在超時時間內被中斷3.超時間結束沒有獲取到鎖,返回false |
void unLock | 釋放鎖 |
Condition newCondition() | 獲取等待通知的組件,該組件和當前鎖綁定,只有獲取到鎖調用wait方法後當前線程將放棄鎖,後續被其他線程signal繼續爭搶鎖 |
2.Lock相比synchronized具備的特性
- 嘗試非阻塞的獲取鎖
- 響應中斷的獲取鎖
- 超時獲取鎖
synchronized相比於Lock 更加簡單,更不容易犯錯,但是不夠靈活
3.使用Lock的經典範式
獲取鎖的過程不要寫在try中,避免獲取鎖失敗最後finally釋放其他線程持有的鎖
二丶AbstractQueuedSynchronizer隊列同步器
使用一個int成員變數state表示同步狀態,內置的FIFO隊列來完成資源的獲取和線程的排隊工作,支持獨占也支持共用的獲取同步狀態。
三個變數被volatile修飾,保證其線程可見性
1.隊列同步器可以被重寫的方法
方法 | 說明 |
---|---|
protected boolean tryAcquire(int arg) | 獨占的獲取鎖,需要查詢當前狀態並判斷同步狀態是否符合預期,然後再進行CAS改變同步狀態 |
protected boolean tryRelease(int arg) | 獨占式釋放同步狀態,等待獲取同步狀態的線程將有機會獲取同步狀態 |
protected int tryAcquireShared(int arg) | 共用式的獲取同步狀態,放回大於等於0()的值表示成功,反之失敗 |
protected boolean tryReleaseShared(int arg) | 共用式釋放同步狀態 |
protected boolean isHeldExclusively() | 當前隊列同步器釋放再獨占模式下被線程占用,一般表示當前線程是否獨占 |
2.隊列同步器提供的模板方法
方法 | 說明 |
---|---|
void acquire(int arg) | 獨占式獲取同步狀態,如果獲取成功那麼直接返回,反之進入同步隊列中等待, |
void acquireInterruptibly(int arg) | 和acquire,但是此方法支持在獲取鎖的過程中響應中斷,如果當前線程被中斷那麼拋出InterruptedException |
boolean tryAcquireNanos(int arg, long nanosTimeout) | 在acquireInterruptibly的基礎上增加了超時限制,如果在指定時間內沒有獲取到同步狀態那麼返回false反之true |
void acquireShared(int arg) | 共用式獲取同步狀態,如果沒有獲取到那麼進入等待隊列等待,和acquire不同的式支持同一個時刻多個線程獲取同步狀態 |
void acquireSharedInterruptibly(int arg) | 和acquireShared類似但是支持響應中斷 |
boolean tryAcquireSharedNanos(int arg, long nanosTimeout) | 在acquireSharedInterruptibly新增了超時限制 |
boolean release(int arg) | 獨占式釋放同步資源,在釋放同步狀態後喚醒後繼線程 |
boolean releaseShared(int arg) | 共用式釋放同步狀態 |
Collection<Thread> getQueuedThreads() |
獲取等待在同步隊列上的線程們 |
3.同步隊列的節點屬性
屬性 | 描述 |
---|---|
int waitStatus | 等待狀態 |
Node pre | 前驅節點 |
Node next | 後繼節點 |
Node nextWaiter | 等待隊列中的後繼節點,如果當前節點式共用模式,那麼這個節點是SHARED常量,也就是說節點類型和等待中後繼節點是公用一個欄位 |
Thread thread | 獲取同步狀態的線程 |
等待狀態是一個枚舉,具備下列可選的值
- CANCELLED(1)線程獲取鎖超時or被中斷,需要從同步隊列中取消中斷,節點進入改狀態後狀態不再改變
- SIGNAL(-1)後繼節點線程處於等待狀態,而當前節點的線程如果釋放共用資源或者被取消會通知後繼節點,使後繼線程被喚醒繼續執行
- CONDITION(-2)節點在等待隊列中,節點中的線程在Condition上進行等待,需要等待其他線程調用Condition的singal or singalAll進行喚醒,該節點會從等待隊列移動到同步隊列,進行共用資源的爭奪
- PROPAGATE(-3)表示下一次共用式同步狀態的獲取將無條件的傳播下去
- 0 初始狀態,節點假如到同步隊列時候的狀態
4.AQS怎麼維護同步隊列
AQS中包含兩個節點類型引用:頭節點和尾節點。當一個線程獲取到同步狀態的時候,其他線程無法獲取,將被放入到同步隊列中,加入隊列這個過程為了保證線程安全而採用CAS。同步隊列遵守FIFO,頭節點是獲取到同步狀態的線程,釋放同步狀態將會喚醒後繼線程,後繼節點獲取到同步狀態後將被設置為頭節點
三丶ReentrantLock可重入鎖
1.ReentrantLock簡介
支持公平和非公平和重入的獨占式鎖
- 重入表示已經獲得鎖的線程可以對共用資源重覆加鎖
- 公平鎖,支持先來後到,像在公司排隊上廁所,先來的人肯定優先獲取到茅坑,先來的線程肯定先獲取到共用資源
- 獨占式,同一時間只允許一個線程操作共用資源
2.公平鎖和非公平鎖比較
公平鎖在頭節點釋放同步資源的時候需要unpark後續節點,並切換線程執行上下文,導致效率並不如非公平鎖,但是公平鎖可以減少饑餓,因為非公平鎖好像A在排隊,A獲取到共用資源需要進行喚醒和上下文切換,而導致需要更多時間,這時候流氓B剛好進廁所門,上來就是一個CAS,很快搶占了廁所這一共用資源,導致A處於饑餓
——遲遲得不到廁所(共用資源)的操作資源。
3.ReentrantLock的可重入
實現可重入需要解決兩個問題
- 線程再次獲得鎖,鎖需要去識別當前線程釋放是當前占據鎖的線程,如果是那麼直接加鎖成功
- 鎖的最終釋放,加鎖多少次,就需要釋放多少次,完全解鎖後其他線程才可以獲取到鎖。
第一個問題ReentrantLock通過獲取當前線程和獨占鎖線程的`==1判斷來實現,第二個問題ReentrantLock通過對AQS中的共用資源state增加和減少來實現
四丶結合ReentrantLock分析加鎖解鎖的流程
1.ReentranLock
ReentrantLock的公平和非公平就是由於sync引用指向了的不同實現,其lock unlock等操作也是一律交由到sync
2.ReentrantLock的非公平模式
2.1非公平加鎖——lock方法
加鎖的大致流程
- 無論是非公平還是公平在加鎖成功後都會通過setExclusiveOwnerThread設置當前線程為獨占鎖的線程,這個方法會記住當前線程,這是後面實現可重入的關鍵、
- acquire 方法會調用tryAcquire方法,這個方法由AQS的子類實現,NonfairSync這裡會調用nonfairTryAcquire方法
2.1.1不公平的嘗試獲取共用資源nonfairTryAcquire
-
如果nonfairTryAcquire返回true表示當前線程獲取到了鎖,那麼皆大歡喜,當前線程可以繼續運行
-
返回false的情況
- 共用資源是0,但是同一個時間多個線程搶占,當前這個線程CAS失敗了
- 共用資源不是0,當前線程也不是獨占的線程
這兩種情況都需要繼續執行AQS的acquire方法
2.1.2AQS的acquire 方法
獨占模式獲取共用資源,對中斷不敏感,或者說不響應中斷——獲取共用資源失敗的線程將會進入到同步隊列,後續對此線程進行中斷操作,線程不會從同步隊列中移出
1.執行流程
2.將當前線程包裝成Node加入到隊列尾addWaiter
-
快速入隊
下麵這段代碼值得品一品
Node pred = tail; if (pred != null) { //當前線程的前置設置為尾,這一步那麼多個線程執行這一步也是無關緊要的 //只是把當前節點的前置改變了,不是改變pred的next指向 node.prev = pred; //CAS設置尾節點 為當前節點,這個自選操作compareAndSetTail是線程安全,同一時間只有一個線程可以設置自己為尾節點 if (compareAndSetTail(pred, node)) { //註意 如果原尾節點是S,線程A設置成功 那麼尾巴被修改為了A,假如A執行下麵一行的時候消耗完了時間片,線程B進來了,這時候線程B拿到的tail就是A,所以不會存線上程安全問題 pred.next = node; return node; } }
-
完整入隊
!
完整入隊和快速入隊差不多,就是多了一個初始化的邏輯
那麼為什麼不直接完整入隊,也許是for迴圈比if多更多的位元組碼需要執行?也許Doug Lea測試多次後發現快速入隊後完整入隊,比直接完整入隊效率更高
3.嘗試出隊acquireQueued
-
如何從自旋中退出
前繼節點是頭節點,頭節點是當前獲取到共用資源的節點,且獲取共用資源tryAcquire成功
-
掛起當前線程避免無休止的自選
自選是cpu操作,無限制的自選是很浪費cpu資源的
如果shouldParkAfterFailedAcquire放回true 表示當前線程需要被掛起,會繼續執行parkAndCheckInterrupt,這個方法很簡單隻有兩行
private final boolean parkAndCheckInterrupt() {
//掛起當前線程
LockSupport.park(this);
//返回中斷狀態,並且清除中斷標識
return Thread.interrupted();
}
如果parkAndCheckInterrupt 返回了true 表示當前線程被中斷過,並且會讓外層的acquireQueued返回true,會導致acquire執行當前線程的自我中斷
理解這一段代碼需要對java中斷機制具備一定理解
java線程中斷機制
-
調用Thread的interrupt方法
- 如果線程處於Running狀態那麼只是修改Thread內部的中斷標識值為true
- 如果線程由於sleep,wait,join等方法進入等待狀態,會直接拋出中斷異常並清楚中斷標識
- 如果線程由於LockSupport.park進入等待狀態,調用該線程的interrupt方法只會讓LockSupport.park返回
-
interrupt,interrupted,isInterrupted三個方法比較
-
interrupt 見上⬆
-
interrupted 返回當前線程的中斷標識並且充值中斷標識
-
isInterrupted返回中斷標識
-
我們繼續說為什麼當前線程在獲取鎖的途中被中斷,需要自我中斷以下
acquire的"需求":
獨占模式獲取共用資源,對中斷不敏感,或者說不響應中斷——獲取共用資源失敗的線程將會進入到同步隊列,後續對此線程進行中斷操作,線程不會從同步隊列中移出
線程獲取同步狀態的時候被中斷會發生什麼——從LockSupport.park(this)中返回繼續拿鎖,這就是為什麼說acquire的對中斷不敏感。
LockSupport.park();不會拋出受檢查異常,當出現被打斷的情況下,線程被喚醒後,我們可以通過Interrupt的狀態來判斷,我們的線程是不是被interrupt的還是被unpark或者到達指定休眠時間
假如我們寫如下這樣的代碼執行
存在一個調度線程中斷了上面的線程,但是上面的線程還在搶奪鎖,並且被park了,這時候上麵線程的park會返回,並且清除中斷標識,如果不進行自我中斷,那麼下麵while內容還是會進行,那麼我們調度線程的中斷就無效了
3.ReentrantLock的公平模式
傳入true獲取一個公平鎖
3.1公平的獲取鎖——lock方法
公平鎖的lock方法直接調用AQS的acquire方法,上面我們分析的acquire方法它會先去調用tryAcquire,這個tryAcquire被FairSync重寫
- FairSync的tryAcquire方法
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
//共用狀態當前空閑
if (c == 0) {
//前面沒有節點 這就是公平是怎麼實現的
//且cas成功 那麼拿到鎖
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;
}
}
源碼沒什麼很難的點,就是通過判斷前面時候還有節點(標識是否由線程比當前線程先到)如果沒有那麼再去拿鎖,如果共用狀態不是0且當前線程不是獨占的線程那麼就會執行acquireQueued方法,在acquireQueued裡面自選獲取鎖會判斷前一個節點是否是頭節點且調用tryAcquire
4.釋放鎖
釋放鎖直接調用AQS的release方法,其中tryRelease方法由ReentrantLock中Sync自己實現(公平or非公平都一樣)
public final boolean release(int arg) {
//完全的釋放資源
if (tryRelease(arg)) {
Node h = head;
//頭節點初始化的時候才為0,但是後面如果由節點加入到同步隊列會把前置節點的狀態設置為Singnal
if (h != null && h.waitStatus != 0)
//喚醒後繼節點
unparkSuccessor(h);
return true;
}
return false;
}
4.1 tryRelease
protected final boolean tryRelease(int releases) {
//重入了n次,當前釋放m次 c=n-m
int c = getState() - releases;
//如果不是獨占鎖的線程 那麼拋出異常
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
//是否完全的釋放了鎖
boolean free = false;
//只有剩下的為0 才是完全釋放鎖
if (c == 0) {
//置為true
free = true;
//獨占線程設置為null
setExclusiveOwnerThread(null);
}
//修改state
setState(c);
return free;
}
需要註意的是只有完全的釋放了共用資源,在ReentrantLock里就是加鎖n次解鎖n次,才返回true,才會去喚醒後繼節點
4.2 unparkSuccessor
private void unparkSuccessor(Node node) {
int ws = node.waitStatus;
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
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)//大於0代表放棄了
s = t;
}
if (s != null)
//喚醒
LockSupport.unpark(s.thread);
}
使用 LockSupport.unpark(s.thread)喚醒線程,這裡需要品一品 Doug Lea 他為什麼要從尾部開始喚醒
-
再品入隊
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; }
-
快速入隊
快速入隊要求尾節點不為空,如果尾節點為空那麼說明
- 當前沒有線程競爭,鎖只有一個線程再使用,直接tryAcquire就成功了,所以頭和尾都沒有初始化
-
完整入隊
- 進入完成入隊條件
- 隊列頭和尾沒有初始化
- CAS失敗,也就是說存在比較多的線程在執行快速入隊
private Node enq(final Node node) { for (;;) { Node t = tail; if (t == null) { // Must initialize if (compareAndSetHead(new Node())) tail = head; } else { //假設AB兩個線程現在正在搶鎖 node.prev = t; //CAS設置尾 當前線程A被設置為了尾 if (compareAndSetTail(t, node)) { //假如A執行這一行的時候用完了時間片,輪到了B B把自己設置了尾並且B的前置是A,此時A的前置還沒來得及設置 //如果這個時候進行喚醒,從頭開始遍歷的話會發現沒有後面的節點了 //所以需要從尾開始,找到B,B繼續往前找到A //Doug Lea 永遠的神 t.next = node; return t; } } } }
- 進入完成入隊條件
-
-
為什麼要從尾開始遍歷
5.其他
5.1獨占式嘗試獲取鎖—— tryLock方法
這部分都是調用的nonfairTryAcquire方法,也就是是說無論是公平還是非公平都是直接不公平的獲取資源。tryLock方法是直接嘗試,只有當前共用資源沒有被占用的時候返回true,否則false 並且是立即返回所以無論是公平還是非公平,調用這個方法都是一樣的邏輯——有人占著廁所那就直接回去繼續工作
5.2 獨占式響應中斷的獲取鎖——lockInterruptibly
這個方法直接調用了AQS的acquireInterruptibly(1)
public final void acquireInterruptibly(int arg)
throws InterruptedException {
//如果已經中斷了那麼拋出中斷異常
//Thread.interrupted() 會清除中斷標識,因為拋出InterruptedException就是響應了中斷,
if (Thread.interrupted())
throw new InterruptedException();
//調用公平or非公平自己覆寫的方法
if (!tryAcquire(arg))
//如果嘗試獲取共用資源失敗了 那麼入隊,自旋的共用資源
doAcquireInterruptibly(arg);
}
5.2.1 doAcquireInterruptibly
基本上和acquireQueued差不多,就是自旋時發現中斷了那麼拋出中斷異常,註意parkAndCheckInterrupt是調用的Thread.interrupted(),會清除中斷標識
個人認為
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);
//不是interrupted 而是 isInterrupted
return Thread.isInterrupted();
}
也可以實現功能,無非使用的地方比如doAcquireInterruptibly 要先調用Thread.interrupted()然後拋出異常
並且acquire方法也不要執行自我中斷
5.2.2 cancelAcquire
放棄共用資源的爭搶,一般是等待超時,或者被中斷後響應中斷
總體上就是,如果當前節點前面右節點可以喚醒當前節點的後繼節點,那麼CAS設置,否則直接喚醒後面的節點,並且把自己從隊列移除
5.3超時並響應中斷的獲取鎖——tryLock(long timeout, TimeUnit unit)
直接調用了AQS的tryAcquireNanos方法
public final boolean tryAcquireNanos(int arg, long nanosTimeout)
throws InterruptedException {
//上來就判斷下是否中斷了
if (Thread.interrupted())
throw new InterruptedException();
//嘗試獲取鎖 (調用對應公平和非公平的方法)or doAcquireNanos
return tryAcquire(arg) ||
doAcquireNanos(arg, nanosTimeout);
}
5.3.1doAcquireNanos
大致邏輯還是那些,需要註意的是,nanosTimeout > spinForTimeoutThreshold,剩餘時間大於閾值(1000)才會掛起,如果小於的化還是進行自旋,因為非常短的超時時間無法做到十分精確(掛起和喚醒也是需要時間的)如果還是進行超時等待反而會表現得不精確