面試補習系列:
- 《面試補習》- JVM知識點大梳理
- 《面試補習》- Java鎖知識大梳理
一、鎖的分類
1、樂觀鎖和悲觀鎖
樂觀鎖就是樂觀的認為不會發生衝突,用cas和版本號實現 悲觀鎖就是認為一定會發生衝突,對操作上鎖
1.悲觀鎖
悲觀鎖,總是假設最壞的情況,每次去拿資料的時候都認為別人會修改,所以每次在拿資料的時候都會上鎖,這樣別人想拿這個資料就會阻塞直到它拿到鎖。
傳統的關係型資料庫裡邊就用到了很多這種鎖機制,比如行鎖,表鎖等,讀鎖,寫鎖等,都是在做操作之前先上鎖。
再比如 Java 裡面的同步原語 synchronized 關鍵字的實現也是悲觀鎖。
複製程式碼
適用場景:
比較適合寫入操作比較頻繁的場景,如果出現大量的讀取操作,每次讀取的時候都會進行加鎖,這樣會增加大量的鎖的開銷,降低了系統的吞吐量。
實現方式: synchronized
和Lock
2.樂觀鎖
每次去拿資料的時候都認為別人不會修改,所以不會上鎖,但是在更新的時候會判斷一下在此期間別人有沒有去更新這個資料,可以使用版本號等機制
ABA問題(JDK1.5之後已有解決方案):CAS需要在操作值的時候檢查記憶體值是否發生變化,沒有發生變化才會更新記憶體值。但是如果記憶體值原來是A,後來變成了B,然後又變成了A,那麼CAS進行檢查時會發現值沒有發生變化,但是實際上是有變化的。ABA問題的解決思路就是在變數前面新增版本號,每次變數更新的時候都把版本號加一,這樣變化過程就從“A-B-A”變成了“1A-2B-3A”。
迴圈時間長開銷大:CAS操作如果長時間不成功,會導致其一直自旋,給CPU帶來非常大的開銷。
只能保證一個共享變數的原子操作(JDK1.5之後已有解決方案):對一個共享變數執行操作時,CAS能夠保證原子操作,但是對多個共享變數操作時,CAS是無法保證操作的原子性的。
複製程式碼
適用場景:
比較適合讀取操作比較頻繁的場景,如果出現大量的寫入操作,資料發生衝突的可能性就會增大,為了保證資料的一致性,應用層需要不斷的重新獲取資料,這樣會增加大量的查詢操作,降低了系統的吞吐量。
實現方式:
1、使用版本標識來確定讀到的資料與提交時的資料是否一致。提交後修改版本標識,不一致時可以採取丟棄和再次嘗試的策略。
2、Java 中的 Compare and Swap 即 CAS ,當多個執行緒嘗試使用 CAS 同時更新同一個變數時,只有其中一個執行緒能更新變數的值,而其它執行緒都失敗,失敗的執行緒並不會被掛起,而是被告知這次競爭中失敗,並可以再次嘗試。
3、在 Java 中 java.util.concurrent.atomic
包下面的原子變數類就是使用了樂觀鎖的一種實現方式 CAS 實現的。
2、公平鎖/非公平鎖
公平鎖:
指多個執行緒按照申請鎖的順序來獲取鎖。
複製程式碼
非公平鎖:
指多個執行緒獲取鎖的順序並不是按照申請鎖的順序,有可能後申請的執行緒比先申請的執行緒優先獲取鎖。
有可能,會造成優先順序反轉或者飢餓現象。
複製程式碼
擴充執行緒飢餓
:
一個或者多個執行緒因為種種原因無法獲得所需要的資源,導致一直無法執行的狀態
導致無法獲取的原因:
執行緒優先順序較低,沒辦法獲取cpu時間
其他執行緒總是能在它之前持續地對該同步塊進行訪問。
執行緒在等待一個本身也處於永久等待完成的物件(比如呼叫這個物件的 wait 方法),因為其他執行緒總是被持續地獲得喚醒。
複製程式碼
實現方式: ReenTrantLock
(公平/非公平)
對於Java ReentrantLock
而言,通過建構函式指定該鎖是否是公平鎖,預設是非公平鎖。非公平鎖的優點在於吞吐量比公平鎖大。
對於Synchronized
而言,也是一種非公平鎖。由於其並不像ReentrantLock
是通過AQS(AbstractQueuedSynchronizer)
的來實現執行緒排程,所以並沒有任何辦法使其變成公平鎖。
3、可重入鎖
如果一個執行緒獲得過該鎖,可以再次獲得,主要是用途就是在遞迴方面,還有就是防止死鎖,比如在一個同步方法塊中呼叫了另一個相同鎖物件的同步方法塊
實現方式: synchronized
、ReentrantLock
4、獨享鎖/共享鎖
獨享鎖是指該鎖一次只能被一個執行緒所持有。
共享鎖是指該鎖可被多個執行緒所持有。
複製程式碼
實現方式:
獨享鎖: ReentrantLock
和 synchronized
貢獻鎖: ReadWriteLock
擴充:
互斥鎖/讀寫鎖 就是對上面的一種具體實現:
互斥鎖:在Java中的具體實現就是ReentrantLock,synchronized
讀寫鎖:在Java中的具體實現就是ReadWriteLock
複製程式碼
對於Java ReentrantLock而言,其是獨享鎖。但是對於Lock的另一個實現類ReadWriteLock,其讀鎖是共享鎖,其寫鎖是獨享鎖。讀鎖的共享鎖可保證併發讀是非常高效的,讀寫,寫讀 ,寫寫的過程是互斥的。獨享鎖與共享鎖也是通過AQS來實現的,通過實現不同的方法,來實現獨享或者共享。對於Synchronized而言,當然是獨享鎖
5、偏向鎖/輕量級鎖/重量級鎖
基於 jdk 1.6 以上
偏向鎖
指的是當前只有這個執行緒獲得,沒有發生爭搶,此時將方法頭的markword設定成0,然後每次過來都cas一下就好,不用重複的獲取鎖.指一段同步程式碼一直被一個執行緒所訪問,那麼該執行緒會自動獲取鎖。降低獲取鎖的代價
輕量級鎖
:在偏向鎖的基礎上,有執行緒來爭搶,此時膨脹為輕量級鎖,多個執行緒獲取鎖時用cas自旋獲取,而不是阻塞狀態
重量級鎖
:輕量級鎖自旋一定次數後,膨脹為重量級鎖,其他執行緒阻塞,當獲取鎖執行緒釋放鎖後喚醒其他執行緒。(執行緒阻塞和喚醒比上下文切換的時間影響大的多,涉及到使用者態和核心態的切換)
實現方式: synchronized
6、分段鎖
在1.7的concurrenthashmap中有分段鎖的實現,具體為預設16個的segement陣列,其中segement繼承自reentranklock,每個執行緒過來獲取一個鎖,然後操作這個鎖下連著的map。
實現方式:
我們以ConcurrentHashMap來說一下分段鎖的含義以及設計思想,ConcurrentHashMap中的分段鎖稱為Segment,
它即類似於HashMap(JDK7與JDK8中HashMap的實現)的結構,即內部擁有一個Entry陣列,陣列中的每個元素又是一個連結串列;
同時又是一個ReentrantLock(Segment繼承了ReentrantLock)。
當需要put元素的時候,並不是對整個hashmap進行加鎖,而是先通過hashcode來知道他要放在那一個分段中,
然後對這個分段進行加鎖,所以當多執行緒put的時候,只要不是放在一個分段中,就實現了真正的並行的插入。
但是,在統計size的時候,可就是獲取hashmap全域性資訊的時候,就需要獲取所有的分段鎖才能統計。
分段鎖的設計目的是細化鎖的粒度,當操作不需要更新整個陣列的時候,就僅僅針對陣列中的一項進行加鎖操作。
複製程式碼
二、鎖的底層實現
1、Synchronized
synchronized 關鍵字通過一對位元組碼指令 monitorenter/monitorexit 實現
前置知識:
物件頭:
Hotspot 虛擬機器的物件頭主要包括兩部分資料:Mark Word(標記欄位)、Klass Pointer(型別指標)。其中:
Klass Point 是是物件指向它的類後設資料的指標,虛擬機器通過這個指標來確定這個物件是哪個類的例項。
Mark Word 用於儲存物件自身的執行時資料,它是實現輕量級鎖和偏向鎖的關鍵,所以下面將重點闡述 Mark Word 。
Monitor:
每一個 Java 物件都有成為Monitor 的潛質,因為在 Java 的設計中 ,每一個 Java 物件自打孃胎裡出來就帶了一把看不見的鎖,它叫做內部鎖或者 Monitor 鎖
複製程式碼
物件頭結構:
Monitor資料結構:
ObjectMonitor() {
_header = NULL;
_count = 0; //記錄個數
_waiters = 0,
_recursions = 0;
_object = NULL;
_owner = NULL;
_WaitSet = NULL; //處於wait狀態的執行緒,會被加入到_WaitSet
_WaitSetLock = 0 ;
_Responsible = NULL ;
_succ = NULL ;
_cxq = NULL ;
FreeNext = NULL ;
_EntryList = NULL ; //處於等待鎖block狀態的執行緒,會被加入到該列表
_SpinFreq = 0 ;
_SpinClock = 0 ;
OwnerIsThread = 0 ;
}
參考: https://blog.csdn.net/javazejian/article/details/72828483
複製程式碼
ObjectMonitor中有兩個佇列,_WaitSet
和 _EntryList
,用來儲存ObjectWaiter
物件列表( 每個等待鎖的執行緒都會被封裝成ObjectWaiter物件),_owner
指向持有ObjectMonitor
物件的執行緒,當多個執行緒同時訪問一段同步程式碼時,首先會進入 _EntryList
集合,當執行緒獲取到物件的monitor
後進入 _Owner
區域並把monitor
中的owner
變數設定為當前執行緒同時monitor中的計數器count加1
.
若執行緒呼叫 wait() 方法,將釋放當前持有的monitor,owner變數恢復為null,count自減1,同時該執行緒進入 WaitSe t集合中等待被喚醒。若當前執行緒執行完畢也將釋放monitor(鎖)並復位變數的值,以便其他執行緒進入獲取monitor(鎖)
這裡比較複雜,但是建議仔細閱讀,便於後續分析的時候理解
複製程式碼
1.1、位元組碼實現
同步程式碼塊:
public class SynchronizedTest {
public void test2() {
synchronized(this) {
}
}
}
複製程式碼
synchronized
關鍵字基於上述兩個指令實現了鎖的獲取和釋放過程:
monitorenter
指令插入到同步程式碼塊的開始位置,
monitorexit
指令插入到同步程式碼塊的結束位置.
執行緒執行到 monitorenter 指令時,將會嘗試獲取物件所對應的 Monitor 所有權,即嘗試獲取物件的鎖。
當執行monitorenter指令時,當前執行緒將試圖獲取 objectref(即物件鎖) 所對應的 monitor 的持有權,當 objectref 的 monitor 的進入計數器為 0,那執行緒可以成功取得 monitor,並將計數器值設定為 1,取鎖成功。如果當前執行緒已經擁有 objectref 的 monitor 的持有權,那它可以重入這個 monitor (關於重入性稍後會分析),重入時計數器的值也會加 1。倘若其他執行緒已經擁有 objectref 的 monitor 的所有權,那當前執行緒將被阻塞,直到正在執行執行緒執行完畢,即monitorexit指令被執行,執行執行緒將釋放 monitor(鎖)並設定計數器值為0 ,其他執行緒將有機會持有 monitor 。
複製程式碼
同步方法:
synchronized 方法則會被翻譯成普通的方法呼叫和返回指令如:
invokevirtual、areturn 指令,在 JVM 位元組碼層面並沒有任何特別的指令來實現被synchronized 修飾的方法,
而是在 Class 檔案的方法表中將該方法的 access_flags 欄位中的 synchronized 標誌位置設定為 1,
表示該方法是同步方法,並使用呼叫該方法的物件或該方法所屬的 Class
在 JVM 的內部物件表示 Klass 作為鎖物件
複製程式碼
//省略沒必要的位元組碼
//==================syncTask方法======================
public synchronized void syncTask();
descriptor: ()V
//方法標識ACC_PUBLIC代表public修飾,ACC_SYNCHRONIZED指明該方法為同步方法
flags: ACC_PUBLIC, ACC_SYNCHRONIZED
Code:
stack=3, locals=1, args_size=1
0: aload_0
1: dup
2: getfield #2 // Field i:I
5: iconst_1
6: iadd
7: putfield #2 // Field i:I
10: return
LineNumberTable:
line 12: 0
line 13: 10
}
SourceFile: "SyncMethod.java"
複製程式碼
以下部分參考: JVM原始碼分析之synchronized實現
1.2、偏向鎖獲取
1、獲取物件頭的Mark Word;
2、判斷mark是否為可偏向狀態,即mark的偏向鎖標誌位為 1,鎖標誌位為 01;
3、判斷mark中JavaThread的狀態:如果為空,則進入步驟(4);如果指向當前執行緒,
則執行同步程式碼塊;如果指向其它執行緒,進入步驟(5);
4、通過CAS原子指令設定mark中JavaThread為當前執行緒ID,
如果執行CAS成功,則執行同步程式碼塊,否則進入步驟(5);
5、如果執行CAS失敗,表示當前存在多個執行緒競爭鎖,當達到全域性安全點(safepoint),
獲得偏向鎖的執行緒被掛起,撤銷偏向鎖,並升級為輕量級,升級完成後被阻塞在安全點的執行緒繼續執行同步程式碼塊;
複製程式碼
在大多數情況下,鎖不僅不存在多執行緒競爭,而且總是由同一執行緒多次獲得,因此為了減少同一執行緒獲取鎖(會涉及到一些CAS操作,耗時)的代價而引入偏向鎖。偏向鎖的核心思想是,如果一個執行緒獲得了鎖,那麼鎖就進入偏向模式,此時Mark Word 的結構也變為偏向鎖結構,當這個執行緒再次請求鎖時,無需再做任何同步操作,即獲取鎖的過程,這樣就省去了大量有關鎖申請的操作,從而也就提供程式的效能。所以,對於沒有鎖競爭的場合,偏向鎖有很好的優化效果,畢竟極有可能連續多次是同一個執行緒申請相同的鎖。
注意 JVM 提供了關閉偏向鎖的機制, JVM 啟動命令指定如下引數即可
-XX:-UseBiasedLocking
複製程式碼
偏向鎖的撤銷:
偏向鎖的 撤銷(revoke) 是一個很特殊的操作, 為了執行撤銷操作, 需要等待全域性安全點(Safe Point),
此時間點所有的工作執行緒都停止了位元組碼的執行。
偏向鎖這個機制很特殊, 別的鎖在執行完同步程式碼塊後, 都會有釋放鎖的操作, 而偏向鎖並沒有直觀意義上的“釋放鎖”操作。
引入一個概念 epoch, 其本質是一個時間戳 , 代表了偏向鎖的有效性
複製程式碼
1.3、輕量級鎖
在多執行緒交替執行同步塊的情況下,儘量避免重量級鎖引起的效能消耗,但是如果多個執行緒在同一時刻進入臨界區,會導致輕量級鎖膨脹升級重量級鎖,所以輕量級鎖的出現並非是要替代重量級鎖。
1、獲取物件的markOop資料mark;
2、判斷mark是否為無鎖狀態:mark的偏向鎖標誌位為 0,鎖標誌位為 01;
3、如果mark處於無鎖狀態,則進入步驟(4),否則執行步驟(6);
4、把mark儲存到BasicLock物件的_displaced_header欄位;
5、通過CAS嘗試將Mark Word更新為指向BasicLock物件的指標,如果更新成功,表示競爭到鎖,則執行同步程式碼,否則執行步驟(6);
6、如果當前mark處於加鎖狀態,且mark中的ptr指標指向當前執行緒的棧幀,則執行同步程式碼,否則說明有多個執行緒競爭輕量級鎖,輕量級鎖需要膨脹升級為重量級鎖;
複製程式碼
1.4、重量級鎖
重量級鎖通過物件內部的監視器(monitor)實現,其中monitor的本質是依賴於底層作業系統的Mutex Lock實現,作業系統實現執行緒之間的切換需要從使用者態到核心態的切換,切換成本非常高。
鎖膨脹過程:
1、整個膨脹過程在自旋下完成;
2、mark->has_monitor()方法判斷當前是否為重量級鎖,即Mark Word的鎖標識位為 10,如果當前狀態為重量級鎖,執行步驟(3),否則執行步驟(4);
3、mark->monitor()方法獲取指向ObjectMonitor的指標,並返回,說明膨脹過程已經完成;
4、如果當前鎖處於膨脹中,說明該鎖正在被其它執行緒執行膨脹操作,則當前執行緒就進行自旋等待鎖膨脹完成,這裡需要注意一點,
雖然是自旋操作,但不會一直佔用cpu資源,每隔一段時間會通過os::NakedYield方法放棄cpu資源,或通過park方法掛起;
如果其他執行緒完成鎖的膨脹操作,則退出自旋並返回;
5、如果當前是輕量級鎖狀態,即鎖標識位為 00
複製程式碼
Monitor 競爭:
1、通過CAS嘗試把monitor的_owner欄位設定為當前執行緒;
2、如果設定之前的_owner指向當前執行緒,說明當前執行緒再次進入monitor,即重入鎖,執行_recursions ++ ,記錄重入的次數;
3、如果之前的_owner指向的地址在當前執行緒中,這種描述有點拗口,換一種說法:之前_owner指向的BasicLock在當前執行緒棧上,
說明當前執行緒是第一次進入該monitor,設定_recursions為1,_owner為當前執行緒,該執行緒成功獲得鎖並返回;
4、如果獲取鎖失敗,則等待鎖的釋放;
複製程式碼
其本質就是通過CAS設定monitor的_owner欄位為當前執行緒,如果CAS成功,則表示該執行緒獲取了鎖,跳出自旋操作,執行同步程式碼,否則繼續被掛起;
Monitor 釋放:
當某個持有鎖的執行緒執行完同步程式碼塊時,會進行鎖的釋放,給其它執行緒機會執行同步程式碼,在HotSpot中,通過退出monitor的方式實現鎖的釋放,並通知被阻塞的執行緒.
1.5、鎖優化內容
鎖消除:
消除鎖是虛擬機器另外一種鎖的優化,這種優化更徹底,
Java虛擬機器在JIT編譯時(可以簡單理解為當某段程式碼即將第一次被執行時進行編譯,又稱即時編譯),
通過對執行上下文的掃描,去除不可能存在共享資源競爭的鎖,
通過這種方式消除沒有必要的鎖,可以節省毫無意義的請求鎖時間
複製程式碼
鎖粗化:
將多個連續的加鎖、解鎖操作連線在一起,擴充套件成一個範圍更大的鎖。
複製程式碼
自旋鎖:
執行緒的阻塞和喚醒,需要 CPU 從使用者態轉為核心態。頻繁的阻塞和喚醒對 CPU 來說是一件負擔很重的工作,勢必會給系統的併發效能帶來很大的壓力。
同時,我們發現在許多應用上面,物件鎖的鎖狀態只會持續很短一段時間。為了這一段很短的時間,頻繁地阻塞和喚醒執行緒是非常不值得的
適應性自旋鎖:
自適應就意味著自旋的次數不再是固定的,它是由前一次在同一個鎖上的自旋時間及鎖的擁有者的狀態來決定
複製程式碼
鎖升級:
2、ReetrantLock
2.1、Lock
//加鎖
void lock();
//解鎖
void unlock();
//可中斷獲取鎖,與lock()不同之處在於可響應中斷操作,即在獲
//取鎖的過程中可中斷,注意synchronized在獲取鎖時是不可中斷的
void lockInterruptibly() throws InterruptedException;
//嘗試非阻塞獲取鎖,呼叫該方法後立即返回結果,如果能夠獲取則返回true,否則返回false
boolean tryLock();
//根據傳入的時間段獲取鎖,在指定時間內沒有獲取鎖則返回false,如果在指定時間內當前執行緒未被中並斷獲取到鎖則返回true
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
//獲取等待通知元件,該元件與當前鎖繫結,當前執行緒只有獲得了鎖
//才能呼叫該元件的wait()方法,而呼叫後,當前執行緒將釋放鎖。
Condition newCondition();
複製程式碼
在Java 1.5中,官方在concurrent併發包(J.U.C)
中加入了Lock介面,該介面中提供了lock()方法和unLock()方法對顯式加鎖和顯式釋放鎖操作進行支援.
Lock 鎖提供的優勢:
可以使鎖更公平。
可以使執行緒在等待鎖的時候響應中斷。
可以讓執行緒嘗試獲取鎖,並在無法獲取鎖的時候立即返回或者等待一段時間。
可以在不同的範圍,以不同的順序獲取和釋放鎖。
複製程式碼
2.2、AQS (AbstractQueuedSynchronizer)
AQS 即佇列同步器。它是構建鎖或者其他同步元件的基礎框架(如 ReentrantLock、ReentrantReadWriteLock、Semaphore 等),J.U.C 併發包的作者(Doug Lea)期望它能夠成為實現大部分同步需求的基礎。
資料結構:
//同步佇列頭節點
private transient volatile Node head;
//同步佇列尾節點
private transient volatile Node tail;
//同步狀態
private volatile int state;
複製程式碼
AQS 使用一個 int 型別的成員變數 state 來表示同步狀態:
- 當
state > 0
時,表示已經獲取了鎖。 - 當
state = 0
時,表示釋放了鎖。
Node構成FIFO的同步佇列來完成執行緒獲取鎖的排隊工作
- 如果當前執行緒獲取同步狀態失敗(鎖)時,AQS 則會將當前執行緒以及等待狀態等資訊構造成一個節點(Node)並將其加入同步佇列,同時會阻塞當前執行緒
- 當同步狀態釋放時,則會把節點中的執行緒喚醒,使其再次嘗試獲取同步狀態
參考: 深入剖析基於併發AQS的(獨佔鎖)重入鎖(ReetrantLock)及其Condition實現原理
2.3、Sync
Sync
:抽象類,是ReentrantLock的內部類,繼承自AbstractQueuedSynchronizer,實現了釋放鎖的操作(tryRelease()方法),並提供了lock抽象方法,由其子類實現。
NonfairSync
:是ReentrantLock的內部類,繼承自Sync,非公平鎖的實現類。
FairSync
:是ReentrantLock的內部類,繼承自Sync,公平鎖的實現類。
AQS、Sync 和 ReentrantLock 的具體關係圖:
2.4、ReentrantLock 實現原理
建構函式:
public ReentrantLock() {
sync = new NonfairSync();
}
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
複製程式碼
ReentrantLock 提供兩種實現方式,公平鎖/非公平鎖. 通過建構函式進行初始化 sync
進行判斷當前鎖得型別.
2.4.1、非公平鎖(NonfairSync)
final void lock() {
//cas 獲取鎖
if (compareAndSetState(0, 1))
//如果成功設定當前執行緒Id
setExclusiveOwnerThread(Thread.currentThread());
else
//否則再次請求同步狀態
acquire(1);
}
複製程式碼
先對同步狀態執行CAS操作,嘗試把state的狀態從0設定為1,
如果返回true則代表獲取同步狀態成功,也就是當前執行緒獲取鎖成,可操作臨界資源,如果返回false,則表示已有執行緒持有該同步狀態(其值為1)
獲取鎖失敗,注意這裡存在併發的情景,也就是可能同時存在多個執行緒設定state變數,因此是CAS操作保證了state變數操作的原子性。返回false後,執行acquire(1)
方法
#acquire(int arg)
方法,為 AQS 提供的模板方法。該方法為獨佔式獲取同步狀態,但是該方法對中斷不敏感。也就是說,由於執行緒獲取同步狀態失敗而加入到 CLH 同步佇列中,後續對該執行緒進行中斷操作時,執行緒不會從 CLH 同步佇列中移除。
acquire
程式碼:
public final void acquire(int arg) {
//嘗試獲取同步狀態
if (!tryAcquire(arg) &&
//自旋直到獲得同步狀態成功,新增節點到佇列
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
複製程式碼
1、tryAcquire
嘗試獲取同步狀態
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
//鎖閒置
if (c == 0) {
//CAS佔用
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
//如果鎖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;
}
複製程式碼
2、acquireQueued
加入佇列中,自旋獲取鎖
private Node addWaiter(Node mode) {
//將請求同步狀態失敗的執行緒封裝成結點
Node node = new Node(Thread.currentThread(), mode);
Node pred = tail;
//如果是第一個結點加入肯定為空,跳過。
//如果非第一個結點則直接執行CAS入隊操作,嘗試在尾部快速新增
if (pred != null) {
node.prev = pred;
//使用CAS執行尾部結點替換,嘗試在尾部快速新增
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
//如果第一次加入或者CAS操作沒有成功執行enq入隊操作
enq(node);
return node;
}
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
//獲取前驅節點
final Node p = node.predecessor();
//如果前驅節點試頭節點, 嘗試獲取同步狀態
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
// 獲取失敗,執行緒等待
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
複製程式碼
流程圖:
2.4.2、公平鎖(FairSync)
與非公平鎖不同的是,在獲取鎖的時,公平鎖的獲取順序是完全遵循時間上的FIFO規則,也就是說先請求的執行緒一定會先獲取鎖,後來的執行緒肯定需要排隊,這點與前面我們分析非公平鎖的nonfairTryAcquire(int acquires)方法實現有鎖不同,下面是公平鎖中tryAcquire()方法的實現
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
//判斷佇列中是否又執行緒在等待
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;
}
複製程式碼
2.4.3、解鎖
//ReentrantLock類的unlock
public void unlock() {
sync.release(1);
}
//AQS類的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;
}
//ReentrantLock類中的內部類Sync實現的tryRelease(int releases)
protected final boolean tryRelease(int releases) {
int c = getState() - releases;
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
//判斷狀態是否為0,如果是則說明已釋放同步狀態
if (c == 0) {
free = true;
//設定Owner為null
setExclusiveOwnerThread(null);
}
//設定更新同步狀態
setState(c);
return free;
}
複製程式碼
3、ReentrantReadWriteLock
建構函式:
Lock readLock();
Lock writeLock();
/** 使用預設(非公平)的排序屬性建立一個新的 ReentrantReadWriteLock */
public ReentrantReadWriteLock() {
this(false);
}
/** 使用給定的公平策略建立一個新的 ReentrantReadWriteLock */
public ReentrantReadWriteLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
readerLock = new ReadLock(this);
writerLock = new WriteLock(this);
}
複製程式碼
java.util.concurrent.locks.ReentrantReadWriteLock
,實現 ReadWriteLock
介面,可重入的讀寫鎖實現類。在它內部,維護了一對相關的鎖,一個用於只讀操作,另一個用於寫入操作。只要沒有 Writer
執行緒,讀取鎖可以由多個 Reader
執行緒同時保持。也就說說,寫鎖是獨佔的,讀鎖是共享的。
在 ReentrantLock 中,使用 Sync ( 實際是 AQS )的 int 型別的 state 來表示同步狀態,表示鎖被一個執行緒重複獲取的次數。但是,讀寫鎖 ReentrantReadWriteLock 內部維護著一對讀寫鎖,如果要用一個變數維護多種狀態,需要採用“按位切割使用”的方式來維護這個變數,將其切分為兩部分:高16為表示讀,低16為表示寫。
分割之後,讀寫鎖是如何迅速確定讀鎖和寫鎖的狀態呢?通過位運算。假如當前同步狀態為S,那麼:
- 寫狀態,等於 S & 0x0000FFFF(將高 16 位全部抹去)
- 讀狀態,等於 S >>> 16 (無符號補 0 右移 16 位)。
1、readLock
public final void acquireShared(int arg) {
if (tryAcquireShared(arg) < 0)
doAcquireShared(arg);
}
protected final int tryAcquireShared(int unused) {
//當前執行緒
Thread current = Thread.currentThread();
int c = getState();
//exclusiveCount(c)計算寫鎖
//如果存在寫鎖,且鎖的持有者不是當前執行緒,直接返回-1
//存在鎖降級問題,後續闡述
if (exclusiveCount(c) != 0 &&
getExclusiveOwnerThread() != current)
return -1;
//讀鎖
int r = sharedCount(c);
/*
* readerShouldBlock():讀鎖是否需要等待(公平鎖原則)
* r < MAX_COUNT:持有執行緒小於最大數(65535)
* compareAndSetState(c, c + SHARED_UNIT):設定讀取鎖狀態
*/
if (!readerShouldBlock() &&
r < MAX_COUNT &&
compareAndSetState(c, c + SHARED_UNIT)) { //修改高16位的狀態,所以要加上2^16
/*
* holdCount部分後面講解
*/
if (r == 0) {
firstReader = current;
firstReaderHoldCount = 1;
} else if (firstReader == current) {
firstReaderHoldCount++;
} else {
HoldCounter rh = cachedHoldCounter;
if (rh == null || rh.tid != getThreadId(current))
cachedHoldCounter = rh = readHolds.get();
else if (rh.count == 0)
readHolds.set(rh);
rh.count++;
}
return 1;
}
return fullTryAcquireShared(current);
}
複製程式碼
4、synchronized 和 ReentrantLock 異同?
相同點
都實現了多執行緒同步和記憶體可見性語義。
都是可重入鎖。
複製程式碼
不同點
同步實現機制不同
synchronized 通過 Java 物件頭鎖標記和 Monitor 物件實現同步。
ReentrantLock 通過CAS、AQS(AbstractQueuedSynchronizer)和 LockSupport(用於阻塞和解除阻塞)實現同步。
可見性實現機制不同
synchronized 依賴 JVM 記憶體模型保證包含共享變數的多執行緒記憶體可見性。
ReentrantLock 通過 ASQ 的 volatile state 保證包含共享變數的多執行緒記憶體可見性。
使用方式不同
synchronized 可以修飾例項方法(鎖住例項物件)、靜態方法(鎖住類物件)、程式碼塊(顯示指定鎖物件)。
ReentrantLock 顯示呼叫 tryLock 和 lock 方法,需要在 finally 塊中釋放鎖。
功能豐富程度不同
synchronized 不可設定等待時間、不可被中斷(interrupted)。
ReentrantLock 提供有限時間等候鎖(設定過期時間)、可中斷鎖(lockInterruptibly)、condition(提供 await、condition(提供 await、signal 等方法)等豐富功能
鎖型別不同
synchronized 只支援非公平鎖。
ReentrantLock 提供公平鎖和非公平鎖實現。當然,在大部分情況下,非公平鎖是高效的選擇。
複製程式碼