本文是自己的學習筆記,主要參考資料如下
JavaSE文檔
- 1、AQS 概述
- 1.1、鎖的原理
- 1.2、任務隊列
- 1.2.1、結點的狀態變化
- 1.3、加鎖和解鎖的簡單流程
- 2、ReentrantLock
- 2.1、加鎖源碼分析
- 2.1.1、tryAcquire()的具體實現
- 2.1.2、acquirQueued()的具體實現
- 2.1.3、tryLock的具體實現
- 2.1.5、總結
1、AQS 概述
1.1、鎖的原理
AQS是指抽象類AbstractQueuedSynchronizer
。這個抽象類代表著一種實現并發的方式。
具體實現方式是使用volitile
修飾state
變量,保證了state
的可見性和有序性。最后使用CAS
改變state
的值,保證原子性。
那么AbstractQueuedSynchronizer
通過更新state
的值來實現的加鎖和解鎖。
下面是關鍵源代碼的截圖。
1.2、任務隊列
AQS
中維護了一個任務隊列,是一個雙向隊列。隊列節點是內部類Node
。
在Node
中記錄者節點的狀態waitStatus
,比如CANCEL
,SIGNAL
等分別表示該任務節點已經取消和任務節點正在沉睡需要被喚醒。
當然,因為是雙向列表所以也有指向前后節點的指針。下面是Node
源碼的部分截圖。
這個隊列會初始化一個頭結點和一個尾結點作為虛擬節點。頭結點的狀態在整個加鎖和釋放鎖的過程中都會變化。
1.2.1、結點的狀態變化
當頭結點指向的Node
才擁有鎖。
這里主要介紹三個狀態
0
, 表示當前Node
后續無節點在排隊。不表明是否擁有鎖。-1
,表示除了當前Node
在排隊以外,還有其他Node
排在當前Node
后面。不表明是否擁有鎖。1
,表示當前Node
可能因為等待時間太長而放棄獲取鎖。
下面是三個Node
在隊列中的狀態。這里從左到右解釋他們的狀態。
head
指向第一個Node
,所以當前Node
擁有鎖。
第一個Node
的waitStatus=-1
表示后續有節點等待獲取鎖。當該節點釋放鎖時會喚醒后續的節點。
第二個Node
的waitStatus = -1
,后續有節點等待獲取鎖。
第三個Node
的waitStatus = 0
,后續無節點等待獲取鎖。
1.3、加鎖和解鎖的簡單流程
假設有兩個線程A和B,他們需要爭奪基于AQS
實現的鎖,下面是爭奪的簡單流程。
- 線程A先執行CAS,將state從0修改為1,線程A就獲取到了鎖資源,去執行業務代碼即可。
- 線程B再執行CAS,發現state已經是1了,無法獲取到鎖資源。
- 線程B需要去排隊,將自己封裝為Node對象。
- 需要將當前B線程的Node放到雙向隊列保存,排隊。
2、ReentrantLock
2.1、加鎖源碼分析
ReentrantLock
分為公平鎖和非公平鎖。在加鎖的時候因這兩種鎖的不同會有不同的加鎖方式。
ReentrantLock
默認是非公平鎖,構造方法中傳入false
則是公平鎖。
非公平鎖的lock()
方法會直接基于CAS
嘗試獲取鎖,如果成功的話則執行setExclusiveOwnerThread()
方法表示當前線程持有該鎖;如果失敗則執行acquire()
方法。
公平鎖則是直接執行acquire()
方法。下面是源碼對比。
接下來的重點則是看acquire()
的具體操作。
tryAcquire()
方法會再次嘗試獲取鎖,如果成功返回true
,否則返回false
。
可以看到如果失敗的話則將請求放到等待隊列中同時發送中斷信號。
2.1.1、tryAcquire()的具體實現
- 非公平鎖
非公平鎖會嘗試再次直接通過CAS
獲取鎖資源。因為是可重入鎖,所以當鎖的持有者是當前線程時也可直接獲取鎖,然后計數器加一。
- 公平鎖
公平鎖的邏輯與非公平鎖類似,只不過再獲取鎖之前會先判斷AQS
中自己是不是排在第一位,之后才會獲取鎖。
2.1.2、acquirQueued()的具體實現
當tryAcquire()
返回false
,即獲取鎖失敗,就開始嘗試將當前線程封裝成Node節點插入到AQS
的結尾。
在插入時我們會看到if(p == head && tryAcquire(arg))
這樣的語句。
這是因為AQS
有偽頭結點,所以當這個線程插入到AQS
中時發現自己的上一個節點是頭結點,即自己排在第一位,那無論是公平鎖還是非公平鎖自己都可以再次測試獲取鎖。所以會再次執行tryAcquire()
。
final boolean acquireQueued(final Node node, int arg) {// 不考慮中斷// failed:獲取鎖資源是否失敗(這里簡單掌握落地,真正觸發的,還是tryLock和lockInterruptibly)boolean failed = true;try {boolean interrupted = false;for (;;) {// 拿到當前節點的前繼節點final Node p = node.predecessor();// 前繼節點是否是head,如果是head,再次執行tryAcquire嘗試獲取鎖資源。if (p == head && tryAcquire(arg)) {// 獲取鎖資源成功setHead(node);p.next = null; // 獲取鎖失敗標識為falsefailed = false;return interrupted;}// 沒拿到鎖資源……// shouldParkAfterFailedAcquire:基于上一個節點轉改來判斷當前節點是否能夠掛起線程,如果可以返回true,// 如果不能,就返回false,繼續下次循環if (shouldParkAfterFailedAcquire(p, node) &&// 這里基于Unsafe類的park方法,將當前線程掛起parkAndCheckInterrupt())interrupted = true;}} finally {if (failed)// 在lock方法中,基本不會執行。cancelAcquire(node);}
}
2.1.3、tryLock的具體實現
無參的tryLock()
比較簡單,和tryAcquire()
基本沒區別。
這里主要講解有參的tryAcquireNanos(int arg, long nanosTimeout)
。
它的作用在一個時間內嘗試獲得鎖。在這個時間內沒有獲得鎖會掛起park
線程。如果成功則返回true
,時間結束還沒有獲得則返回false
。
public boolean tryLock(long timeout, TimeUnit unit)throws InterruptedException {return sync.tryAcquireNanos(1, unit.toNanos(timeout));
}
該方法需要處理中斷異常,和lock()
方法不一樣。
我們繼續深入。
public final boolean tryAcquireNanos(int arg, long nanosTimeout)throws InterruptedException {if (Thread.interrupted())throw new InterruptedException();return tryAcquire(arg) ||doAcquireNanos(arg, nanosTimeout);
}
可以看到,它直接通過線程的中斷標志位決定是否拋出異常。
之后進行tryAcquire()
,這個方法細節上面分析過,它有公平和非公平兩種實現,簡而言之就是非公平直接嘗試CAS
加鎖,公平則是進入隊列排隊。
也就是說,最后它會正常加鎖,只有失敗時才會執行doAcquireNanos()
。所以有參的tryLock()
方法park
線程的細節就在其中。
那下面就看看這個方法的內部。
核心就是線程會被封裝Node
放到隊列中,之后查看時間,如果時間比較長,就park
線程直到時間結束后再嘗試獲取鎖;如果時間比較短,就在死循環中等到時間結束然后再次獲得鎖。
因為park
的線程主要會因兩個動作結束park
,即時間到,或者線程發出中斷狀態,所以最后會查看park
是因為什么結束的。如果是中斷則拋出異常,否則嘗試獲取鎖。
private boolean doAcquireNanos(int arg, long nanosTimeout)throws InterruptedException {// 如果等待時間是0秒,直接告辭,拿鎖失敗 if (nanosTimeout <= 0L)return false;// 設置結束時間。final long deadline = System.nanoTime() + nanosTimeout;// 先扔到AQS隊列final Node node = addWaiter(Node.EXCLUSIVE);// 拿鎖失敗,默認trueboolean failed = true;try {for (;;) {// 如果在AQS中,當前node是head的next,直接搶鎖final Node p = node.predecessor();if (p == head && tryAcquire(arg)) {setHead(node);p.next = null; // help GCfailed = false;return true;}// 結算剩余的可用時間nanosTimeout = deadline - System.nanoTime();// 判斷是否是否用盡的位置if (nanosTimeout <= 0L)return false;// shouldParkAfterFailedAcquire:根據上一個節點來確定現在是否可以掛起線程if (shouldParkAfterFailedAcquire(p, node) &&// 避免剩余時間太少,如果剩余時間少就不用掛起線程nanosTimeout > spinForTimeoutThreshold)// 如果剩余時間足夠,將線程掛起剩余時間LockSupport.parkNanos(this, nanosTimeout);// 如果線程醒了,查看是中斷喚醒的,還是時間到了喚醒的。if (Thread.interrupted())// 是中斷喚醒的!throw new InterruptedException();}} finally {if (failed)cancelAcquire(node);}
}
2.1.5、總結
ReentrantLock
的加鎖有公平鎖和非公平鎖兩種方式。
對于非公平鎖,任務一開始會直接嘗試通過CAS
獲取鎖,失敗后才會進入任務隊列。并且進入的時候會再次嘗試獲取鎖。整個過程并不考慮其他節點等了多久,所以才是非公平鎖。
對于公平鎖,任務會按序先進入任務隊列,直到有人喚醒他們才會開始獲取鎖。