在開始本篇文章的內容講述前,先來回答我一個問題,爲何 JDK 提供一個 synchronized
關鍵字以後還要提供一個 Lock 鎖,這不是畫蛇添足嗎?難道 JDK 設計人員都是沙雕嗎?html
我聽過一句話很是的經典,也是我認爲是每一個人都應該瞭解的一句話:你覺得的並非你覺得的
。明白什麼意思麼?不明白的話,加我微信我告訴你。java
ReentrantLock 位於 java.util.concurrent.locks
包下,它實現了 Lock
接口和 Serializable
接口。面試
ReentrantLock 是一把可重入鎖
和互斥鎖
,它具備與 synchronized 關鍵字相同的含有隱式監視器鎖(monitor)的基本行爲和語義,可是它比 synchronized 具備更多的方法和功能。微信
ReentrantLock 類中帶有兩個構造函數,一個是默認的構造函數,不帶任何參數;一個是帶有 fair 參數的構造函數數據結構
public ReentrantLock() {
sync = new NonfairSync();
}
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
複製代碼
第二個構造函數也是判斷 ReentrantLock 是不是公平鎖的條件,若是 fair 爲 true,則會建立一個公平鎖
的實現,也就是 new FairSync()
,若是 fair 爲 false,則會建立一個 非公平鎖
的實現,也就是 new NonfairSync()
,默認的狀況下建立的是非公平鎖多線程
// 建立的是公平鎖
private ReentrantLock lock = new ReentrantLock(true);
// 建立的是非公平鎖
private ReentrantLock lock = new ReentrantLock(false);
// 默認建立非公平鎖
private ReentrantLock lock = new ReentrantLock();
複製代碼
FairSync 和 NonfairSync 都是 ReentrantLock 的內部類,繼承於 Sync
類,下面來看一下它們的繼承結構,便於梳理。併發
abstract static class Sync extends AbstractQueuedSynchronizer {...}
static final class FairSync extends Sync {...}
static final class NonfairSync extends Sync {...}
複製代碼
在多線程嘗試加鎖時,若是是公平鎖,那麼鎖獲取的機會是相同的。不然,若是是非公平鎖,那麼 ReentrantLock 則不會保證每一個鎖的訪問順序。函數
下面是一個公平鎖
的實現工具
public class MyFairLock extends Thread{
private ReentrantLock lock = new ReentrantLock(true);
public void fairLock(){
try {
lock.lock();
System.out.println(Thread.currentThread().getName() + "正在持有鎖");
}finally {
System.out.println(Thread.currentThread().getName() + "釋放了鎖");
lock.unlock();
}
}
public static void main(String[] args) {
MyFairLock myFairLock = new MyFairLock();
Runnable runnable = () -> {
System.out.println(Thread.currentThread().getName() + "啓動");
myFairLock.fairLock();
};
Thread[] thread = new Thread[10];
for(int i = 0;i < 10;i++){
thread[i] = new Thread(runnable);
}
for(int i = 0;i < 10;i++){
thread[i].start();
}
}
}
複製代碼
不信?不信你輸出試試啊!懶得輸出?就知道你懶得輸出,因此直接告訴你結論吧,結論就是本身試
。源碼分析
試完了嗎?試完了我是不會讓你休息的,過來再試一下非公平鎖的測試和結論,知道怎麼試嗎?上面不是講過要給 ReentrantLock 傳遞一個參數的嗎?你想,傳 true 的時候是公平鎖,那麼反過來不就是非公平鎖了?其餘代碼還用改嗎?不須要了啊。
明白了吧,再來測試一下非公平鎖的流程,看看是否是你想要的結果。
一般狀況下,使用多線程訪問公平鎖的效率會很是低
(一般狀況下會慢不少),可是 ReentrantLock 會保證每一個線程都會公平的持有鎖,線程飢餓的次數比較小
。鎖的公平性並不能保證線程調度的公平性。
此時若是你想了解更多的話,那麼我就從源碼的角度跟你聊聊如何 ReentrantLock 是如何實現這兩種鎖的。
如上圖所示,公平鎖的加鎖流程要比非公平鎖的加鎖流程簡單,下面要聊一下具體的流程了,請小夥伴們備好板凳。
下面先看一張流程圖,這張圖是 acquire 方法的三條主要流程
首先是第一條路線,tryAcquire 方法,顧名思義嘗試獲取,也就是說能夠成功獲取鎖,也能夠獲取鎖失敗。
使用 ctrl+左鍵
點進去是調用 AQS 的方法,可是 ReentrantLock 實現了 AQS 接口,因此調用的是 ReentrantLock 的 tryAcquire 方法;
首先會取得當前線程,而後去讀取當前鎖的同步狀態,還記得鎖的四種狀態嗎?分別是 無鎖、偏向鎖、輕量級鎖和重量級鎖
,若是你不是很明白的話,請參考博主這篇文章(不懂什麼是鎖?看看這篇你就明白了),若是判斷同步狀態是 0 的話,就證實是無鎖的,參考下面這幅圖( 1bit 表示的是是否偏向鎖 )
若是是無鎖(也就是沒有加鎖),說明是第一次上鎖,首先會先判斷一下隊列中是否有比當前線程等待時間更長的線程(hasQueuedPredecessors);而後經過 CAS
方法原子性的更新鎖的狀態,CAS 方法更新的要求涉及三個變量,currentValue(當前線程的值),expectedValue(指望更新的值),updateValue(更新的值)
,它們的更新以下
if(currentValue == expectedValue){
currentValue = updateValue
}
複製代碼
CAS 經過 C 底層機制保證原子性,這個你不須要考慮它。若是既沒有排隊的線程並且使用 CAS 方法成功的把 0 -> 1 (偏向鎖),那麼當前線程就會得到偏向鎖,記錄獲取鎖的線程爲當前線程。
而後咱們看 else if
邏輯,若是讀取的同步狀態是1,說明已經線程獲取到了鎖,那麼就先判斷當前線程是否是獲取鎖的線程,若是是的話,記錄一下獲取鎖的次數 + 1,也就是說,只有同步狀態爲 0 的時候是無鎖狀態。若是當前線程不是獲取鎖的線程,直接返回 false。
acquire 方法會先查看同步狀態是否獲取成功,若是成功則方法結束返回,也就是 !tryAcquire == false
,若失敗則先調用 addWaiter 方法再調用 acquireQueued 方法
而後看一下第二條路線 addWaiter
這裏首先把當前線程和 Node 的節點類型進行封裝,Node 節點的類型有兩種,EXCLUSIVE
和 SHARED
,前者爲獨佔模式,後者爲共享模式,具體的區別咱們會在 AQS 源碼討論,這裏讀者只須要知道便可。
首先會進行 tail 節點的判斷,有沒有尾節點,其實沒有頭節點也就至關於沒有尾節點,若是有尾節點,就會原子性的將當前節點插入同步隊列中,再執行 enq 入隊操做,入隊操做至關於原子性的把節點插入隊列中。
若是當前同步隊列尾節點爲null,說明當前線程是第一個加入同步隊列進行等待的線程。
**在看第三條路線 acquireQueued **
主要會有兩個分支判斷,首先會進行無限循環中,循環中每次都會判斷給定當前節點的先驅節點,若是沒有先驅節點會直接拋出空指針異常,直到返回 true。
而後判斷給定節點的先驅節點是否是頭節點,而且當前節點可否獲取獨佔式鎖,若是是頭節點而且成功獲取獨佔鎖後,隊列頭指針用指向當前節點,而後釋放前驅節點。若是沒有獲取到獨佔鎖,就會進入 shouldParkAfterFailedAcquire
和 parkAndCheckInterrupt
方法中,咱們貼出這兩個方法的源碼
shouldParkAfterFailedAcquire
方法主要邏輯是使用compareAndSetWaitStatus(pred, ws, Node.SIGNAL)
使用CAS將節點狀態由 INITIAL 設置成 SIGNAL,表示當前線程阻塞。當 compareAndSetWaitStatus 設置失敗則說明 shouldParkAfterFailedAcquire 方法返回 false,而後會在 acquireQueued 方法中死循環中會繼續重試,直至compareAndSetWaitStatus 設置節點狀態位爲 SIGNAL 時 shouldParkAfterFailedAcquire 返回 true 時纔會執行方法 parkAndCheckInterrupt 方法。(這塊在後面研究 AQS 會細講)
parkAndCheckInterrupt
該方法的關鍵是會調用 LookSupport.park 方法(關於LookSupport會在之後的文章進行討論),該方法是用來阻塞當前線程。
因此 acquireQueued 主要作了兩件事情:若是當前節點的前驅節點是頭節點,而且可以獲取獨佔鎖,那麼當前線程可以得到鎖該方法執行結束退出
若是獲取鎖失敗的話,先將節點狀態設置成 SIGNAL,而後調用 LookSupport.park
方法使得當前線程阻塞。
若是 !tryAcquire
和 acquireQueued
都爲 true 的話,則打斷當前線程。
那麼它們的主要流程以下(注:只是加鎖流程,並非 lock 全部流程)
非公平鎖的加鎖步驟和公平鎖的步驟只有兩處不一樣,一處是非公平鎖在加鎖前會直接使用 CAS 操做設置同步狀態,若是設置成功,就會把當前線程設置爲偏向鎖的線程;一處是 CAS 操做失敗執行 tryAcquire
方法,讀取線程同步狀態,若是未加鎖會使用 CAS 再次進行加鎖,不會等待 hasQueuedPredecessors
方法的執行,達到只要線程釋放鎖就會加鎖的目的。下面經過源碼和流程圖來詳細理解
這是非公平鎖和公平鎖不一樣的兩處地方,下面是非公平鎖的加鎖流程圖
如下是 JavaDoc 官方解釋:
lockInterruptibly 的中文意思爲若是沒有被打斷,則獲取鎖。若是沒有其餘線程持有該鎖,則獲取該鎖並當即返回,將鎖保持計數設置爲1。若是當前線程已經持有鎖,那麼此方法會馬上返回而且持有鎖的數量會 + 1。若是鎖是由另外一個線程持有的,則出於線程調度目的,當前線程將被禁用,並處於休眠狀態,直到發生如下兩種狀況之一
若是當前線程獲取了鎖,則鎖保持計數將設置爲1。
若是當前線程發生了以下狀況:
那麼當前線程就會拋出InterruptedException
而且當前線程的中斷狀態會清除。
下面看一下它的源碼是怎麼寫的
首先會調用 acquireInterruptibly
這個方法,判斷當前線程是否被中斷,若是中斷拋出異常,沒有中斷則判斷公平鎖/非公平鎖
是否已經獲取鎖,若是沒有獲取鎖(tryAcquire 返回 false)則調用 doAcquireInterruptibly
方法,這個方法和 acquireQueued 方法沒什麼區別,就是線程在等待狀態的過程當中,若是線程被中斷,線程會拋出異常。
下面是它的流程圖
僅僅當其餘線程沒有獲取這把鎖的時候獲取這把鎖,tryLock 的源代碼和非公平鎖的加鎖流程基本一致,它的源代碼以下
ReentrantLock
除了能以中斷的方式去獲取鎖,還能夠以超時等待的方式去獲取鎖,所謂超時等待就是線程若是在超時時間內沒有獲取到鎖,那麼就會返回false
,而不是一直死循環獲取。可使用 tryLock 和 tryLock(timeout, unit)) 結合起來實現公平鎖,像這樣
if (lock.tryLock() || lock.tryLock(timeout, unit)) {...}
複製代碼
若是超過了指定時間,則返回值爲 false。若是時間小於或者等於零,則該方法根本不會等待。
它的源碼以下
首先須要瞭解一下 TimeUnit
工具類,TimeUnit 表示給定粒度單位的持續時間,而且提供了一些用於時分秒跨單位轉換的方法,經過使用這些方法進行定時和延遲操做。
toNanos
用於把 long 型表示的時間轉換成爲納秒,而後判斷線程是否被打斷,若是沒有打斷,則以公平鎖/非公平鎖
的方式獲取鎖,若是可以獲取返回true,獲取失敗則調用doAcquireNanos
方法使用超時等待的方式獲取鎖。在超時等待獲取鎖的過程當中,若是等待時間大於應等待時間,或者應等待時間設置不合理的話,返回 false。
這裏面以超時的方式獲取鎖也能夠畫一張流程圖以下
unlock
和 lock
是一對情侶,它們分不開彼此,在調用 lock 後必須經過 unlock 進行解鎖。若是當前線程持有鎖,在調用 unlock 後,count 計數將減小。若是保持計數爲0就會進行解鎖。若是當前線程沒有持有鎖,在調用 unlock 會拋出 IllegalMonitorStateException
異常。下面是它的源碼
在有了上面閱讀源碼的經歷後,相信你會很快明白這段代碼的意思,鎖的釋放不會區分公平鎖仍是非公平鎖,主要的判斷邏輯就是 tryRelease
方法,getState
方法會取得同步鎖的重入次數,若是是獲取了偏向鎖,那麼可能會屢次獲取,state 的值會大於 1,這時候 c 的值 > 0 ,返回 false,解鎖失敗。若是 state = 1,那麼 c = 0,再判斷當前線程是不是獨佔鎖的線程,釋放獨佔鎖,返回 true,當 head 指向的頭結點不爲 null,而且該節點的狀態值不爲0的話纔會執行 unparkSuccessor 方法,再進行鎖的獲取。
在多線程同時訪問時,ReentrantLock 由最後一次
成功鎖定的線程擁有,當這把鎖沒有被其餘線程擁有時,線程調用 lock()
方法會馬上返回併成功獲取鎖。若是當前線程已經擁有鎖,這個方法會馬上返回。能夠經過 isHeldByCurrentThread
和 getHoldCount
來進行檢查。
首先來看 isHeldByCurrentThread 方法
public boolean isHeldByCurrentThread() {
return sync.isHeldExclusively();
}
複製代碼
根據方法名能夠略知一二,是否被當前線程持有
,它用來詢問鎖是否被其餘線程擁有,這個方法和 Thread.holdsLock(Object)
方法內置的監視器鎖相同,而 Thread.holdsLock(Object) 是 Thread
類的靜態方法,是一個 native
類,它表示的意思是若是當前線程在某個對象上持有 monitor lock(監視器鎖) 就會返回 true。這個類沒有實際做用,僅僅用來測試和調試所用。例如
private ReentrantLock lock = new ReentrantLock();
public void lock(){
assert lock.isHeldByCurrentThread();
}
複製代碼
這個方法也能夠確保重入鎖可以表現出不可重入
的行爲
private ReentrantLock lock = new ReentrantLock();
public void lock(){
assert !lock.isHeldByCurrentThread();
lock.lock();
try {
// 執行業務代碼
}finally {
lock.unlock();
}
}
複製代碼
若是當前線程持有鎖則 lock.isHeldByCurrentThread() 返回 true,不然返回 false。
咱們在瞭解它的用法後,看一下它內部是怎樣實現的,它內部只是調用了一下 sync.isHeldExclusively(),sync
是 ReentrantLock 的一個靜態內部類
,基於 AQS 實現,而 AQS 它是一種抽象隊列同步器,是許多併發實現類的基礎,例如 ReentrantLock/Semaphore/CountDownLatch。sync.isHeldExclusively() 方法以下
protected final boolean isHeldExclusively() {
return getExclusiveOwnerThread() == Thread.currentThread();
}
複製代碼
此方法會在擁有鎖以前先去讀一下狀態,若是當前線程是鎖的擁有者,則不須要檢查。
getHoldCount()
方法和isHeldByCurrentThread
都是用來檢查線程是否持有鎖的方法,不一樣之處在於 getHoldCount() 用來查詢當前線程持有鎖的數量,對於每一個未經過解鎖操做匹配的鎖定操做,線程都會保持鎖定狀態,這個方法也一般用於調試和測試,例如
private ReentrantLock lock = new ReentrantLock();
public void lock(){
assert lock.getHoldCount() == 0;
lock.lock();
try {
// 執行業務代碼
}finally {
lock.unlock();
}
}
複製代碼
這個方法會返回當前線程持有鎖的次數,若是當前線程沒有持有鎖,則返回0。
ReentrantLock 能夠經過 newCondition
方法建立 ConditionObject 對象,而 ConditionObject 實現了 Condition
接口,關於 Condition 的用法咱們後面再講。
查詢是否有任意線程已經獲取鎖,這個方法用來監視系統狀態,而不是用來同步控制,很簡單,直接判斷 state
是否等於0。
這個方法也比較簡單,直接使用 instanceof
判斷是否是 FairSync
內部類的實例
public final boolean isFair() {
return sync instanceof FairSync;
}
複製代碼
判斷同步狀態是否爲0,若是是0,則沒有線程擁有鎖,若是不是0,直接返回獲取鎖的線程。
final Thread getOwner() {
return getState() == 0 ? null : getExclusiveOwnerThread();
}
複製代碼
判斷是否有線程正在等待獲取鎖,若是頭節點與尾節點不相等,說明有等待獲取鎖的線程。
public final boolean hasQueuedThreads() {
return head != tail;
}
複製代碼
判斷給定的線程是否正在排隊,若是正在排隊,返回 true。這個方法會遍歷隊列,若是找到匹配的線程,返回true
public final boolean isQueued(Thread thread) {
if (thread == null)
throw new NullPointerException();
for (Node p = tail; p != null; p = p.prev)
if (p.thread == thread)
return true;
return false;
}
複製代碼
此方法會返回一個隊列長度的估計值,該值只是一個估計值,由於在此方法遍歷內部數據結構時,線程數可能會動態變化。 此方法設計用於監視系統狀態,而不用於同步控制。
public final int getQueueLength() {
int n = 0;
for (Node p = tail; p != null; p = p.prev) {
if (p.thread != null)
++n;
}
return n;
}
複製代碼
返回一個包含可能正在等待獲取此鎖的線程的集合。 由於實際的線程集在構造此結果時可能會動態更改,因此返回的集合只是一個大概的列表集合。 返回的集合的元素沒有特定的順序。
public final Collection<Thread> getQueuedThreads() {
ArrayList<Thread> list = new ArrayList<Thread>();
for (Node p = tail; p != null; p = p.prev) {
Thread t = p.thread;
if (t != null)
list.add(t);
}
return list;
}
複製代碼
那麼你看完源碼分析後,你能總結出 synchronized
和 lock
鎖的實現 ReentrantLock
有什麼異同嗎?
Synchronzied 和 Lock 的主要區別以下:
存在層面:Syncronized 是Java 中的一個關鍵字,存在於 JVM 層面,Lock 是 Java 中的一個接口
鎖的釋放條件:1. 獲取鎖的線程執行完同步代碼後,自動釋放;2. 線程發生異常時,JVM會讓線程釋放鎖;Lock 必須在 finally 關鍵字中釋放鎖,否則容易形成線程死鎖
鎖的獲取: 在 Syncronized 中,假設線程 A 得到鎖,B 線程等待。若是 A 發生阻塞,那麼 B 會一直等待。在 Lock 中,會分狀況而定,Lock 中有嘗試獲取鎖的方法,若是嘗試獲取到鎖,則不用一直等待
鎖的狀態:Synchronized 沒法判斷鎖的狀態,Lock 則能夠判斷
鎖的類型:Synchronized 是可重入,不可中斷,非公平鎖;Lock 鎖則是 可重入,可判斷,可公平鎖
鎖的性能:Synchronized 適用於少許同步的狀況下,性能開銷比較大。Lock 鎖適用於大量同步階段:
Lock 鎖能夠提升多個線程進行讀的效率(使用 readWriteLock)
在競爭不是很激烈的狀況下,Synchronized的性能要優於ReetrantLock,可是在資源競爭很激烈的狀況下,Synchronized的性能會降低幾十倍,可是ReetrantLock的性能能維持常態;
ReetrantLock 提供了多樣化的同步,好比有時間限制的同步,能夠被Interrupt的同步(synchronized的同步是不能Interrupt的)等
面試官可能還會問你 ReentrantLock 的加鎖流程是怎樣的,其實若是你能把源碼給他講出來的話,必定是高分。若是你記不住源碼流程的話能夠記住下面這個簡化版的加鎖流程
若是 lock 加鎖設置成功,設置當前線程爲獨佔鎖的線程;
若是 lock 加鎖設置失敗,還會再嘗試獲取一次鎖數量,
若是鎖數量爲0,再基於 CAS 嘗試將 state(鎖數量)從0設置爲1一次,若是設置成功,設置當前線程爲獨佔鎖的線程;
若是鎖數量不爲0或者上邊的嘗試又失敗了,查看當前線程是否是已是獨佔鎖的線程了,若是是,則將當前的鎖數量+1;若是不是,則將該線程封裝在一個Node內,並加入到等待隊列中去。等待被其前一個線程節點喚醒。
文章參考:
【試驗局】ReentrantLock中非公平鎖與公平鎖的性能測試
第五章 ReentrantLock源碼解析1--得到非公平鎖與公平鎖lock()