題目
Synchronized和ReentryLock
鎖
鎖可以視作訪問共享資料的許可證。鎖能夠保護共享資料以實現執行緒安全,其作用包括保障原子性、保障可見性和保障有序性。
Java平臺中的鎖包括內部鎖(IntrinsicLock)和顯式鎖(ExplicitLock)。內部鎖是透過synchronized關鍵字實現的;顯式鎖是透過java.util.concurrent.locks.Lock介面的實現類(如java.concurrent.locks.ReentrantLock類)實現的。synchronized在軟體層面依賴JVM(jvm指令),而j.u.c.Lock在硬體層面依賴特殊的CPU指令。
鎖的排程
公平排程,多個執行緒申請鎖的時候需要排隊,先到先得,不允許插隊,線上程任務執行時間較長的情況下使用公平排程效率更高,公平排程適合執行緒任務執行時間較長的場景;
非公平排程,多個執行緒申請鎖的時候需要排隊,但是允許插隊,一般情況下效率更高;
Synchronized
Java平臺中的任何一個物件都有唯一一個與之關聯的鎖。這種鎖被稱為監視器(Monitor)或者內部鎖(IntrinsicLock)。內部鎖是一種排他鎖,它能夠保障原子性、可見性和有序性。內部鎖是透過synchronized關鍵字實現的。synchronized關鍵字可以用來修飾方法以及程式碼塊(花括號“{}”包裹的程式碼)。
同步靜態方法,鎖為類物件;如果同步例項方法,鎖為this,即當前物件例項;同步程式碼塊的時候需要指定的鎖控制代碼,作為鎖控制代碼的變數通常採用final修飾
synchronized關鍵字申請鎖和釋放鎖都是JVM代為實施,不需要手動操作,所以稱為內部鎖,內部鎖是使用最方便的鎖。
public class SafeCircularSeqGenerator implements CircularSeqGenerator {
private short sequence = -1;
public synchronized short nextSequence() {
if (sequence >= 999) {
sequence = 0;
} else {
sequence++;
}
return sequence;
}
}
public short nextSequence() {
synchronized (this) {
if (sequence >= 999) {
sequence = 0;
} else {
sequence++;
}
return sequence;
}
}
ReentryLock
顯示鎖最常用的是ReentrantLock,需要建立一個ReentrantLock物件lock,並且在同步程式碼前插入lock.lock()
,在同步程式碼後插入lock.unlock()
;,注意lock.unlock();
需要放在finally裡中,避免鎖洩露;
lock.lock(); // 申請鎖lock
try{
// 在此對共享資料進行訪問
……
}finally{
// 總是在finally塊中釋放鎖,以避免鎖洩漏
lock.unlock(); // 釋放鎖lock
}
對比
兩者都是可重入鎖。內部鎖只支援非公平排程,ReentrantLock同時支援公平和非公平排程。
內部鎖和ReentrantLock兩者效能接近,內部鎖使用簡單,ReentrantLock使用複雜,需要手動釋放鎖。一般情況下使用synchronized內部鎖即可,如果需要使用更加複雜的執行緒協作或者需要公平排程等時候可以考慮使用ReentrantLock。
參考:
《Java多執行緒程式設計實戰指南》黃文海
java多執行緒基礎
記憶體屏障?Java是怎麼實現原子性,有序性,可見性呢?
記憶體屏障
記憶體屏障是對一類僅針對記憶體讀、寫操作指令(Instruction)的跨處理器架構(比如x86、ARM)的比較底層的抽象(或者稱呼)。記憶體屏障是被插入到兩個指令之間進行使用的,其作用是禁止編譯器、處理器重排序從而保障有序性。它在指令序列(如指令1;指令2;指令3)中就像是一堵牆(因此被稱為屏障)一樣使其兩側(之前和之後)的指令無法“穿越”它(一旦穿越了就是重排序了)。但是,為了實現禁止重排序的功能,這些指令也往往具有一個副作用——重新整理處理器快取、沖刷處理器快取,從而保證可見性。不同微架構的處理器所提供的這樣的指令是不同的,並且出於不同的目的使用的相應指令也是不同的。
按照可見性保障來劃分,記憶體屏障可分為載入屏障(LoadBarrier)和儲存屏障(StoreBarrier)。載入屏障的作用是重新整理處理器快取,儲存屏障的作用沖刷處理器快取。
按照有序性保障來劃分,記憶體屏障可以分為獲取屏障(AcquireBarrier)和釋放屏障(ReleaseBarrier)。獲取屏障的使用方式是在一個讀操作(包括Read-Modify-Write以及普通的讀操作)之後插入該記憶體屏障,其作用是禁止該讀操作與其後的任何讀寫操作之間進行重排序,這相當於在進行後續操作之前先要獲得相應共享資料的所有權(這也是該屏障的名稱來源)。釋放屏障的使用方式是在一個寫操作之前插入該記憶體屏障,其作用是禁止該寫操作與其前面的任何讀寫操作之間進行重排序。這相當於在對相應共享資料操作結束後釋放所有權(這也是該屏障的名稱來源)。
內部鎖中記憶體屏障的使用
由於內部鎖的申請與釋放對應的Java虛擬機器位元組碼指令分別是monitorenter和monitorexit,因此習慣上我們用MonitorEnter表示鎖的申請,用MonitorExit表示鎖的釋放。
Java虛擬機器會在MonitorExit(釋放鎖)對應的機器碼指令之後插入一個儲存屏障,這就保障了寫執行緒在釋放鎖之前在臨界區中對共享變數所做的更新對讀執行緒的執行處理器來說是可同步的;相應地,Java虛擬機器會在MonitorEnter(申請鎖)對應的機器碼指令之後臨界區開始之前的地方插入一個載入屏障,這使得讀執行緒的執行處理器能夠將寫執行緒對相應共享變數所做的更新從其他處理器同步到該處理器的快取記憶體中。
Java虛擬機器會在MonitorEnter(它包含了讀操作)對應的機器碼指令之後臨界區開始之前的地方插入一個獲取屏障,並在臨界區結束之後MonitorExit(它包含了寫操作)對應的機器碼指令之前的地方插入一個釋放屏障。因此,這兩種屏障就像是三明治的兩層面包片把火腿夾住一樣把臨界區中的程式碼(指令序列)包括起來。由於獲取屏障禁止了臨界區中的任何讀、寫操作被重排序到臨界區之前的可能性,而釋放屏障又禁止了臨界區中的任何讀、寫操作被重排序到臨界區之後的可能性,因此臨界區內的任何讀、寫操作都無法被重排序到臨界區之外。在鎖的排他性的作用下,這使得臨界區中執行的操作序列具有原子性。因此,寫執行緒在臨界區中對各個共享變數所做的更新會同時對讀執行緒可見,即在讀執行緒看來各個共享變數就像是“一下子”被更新的,於是這些執行緒無從(也無必要)區分這些共享變數是以何種順序被更新的。這使得寫執行緒在臨界區中執行的操作自然而然地具有有序性——讀執行緒對這些操作的感知順序與原始碼順序一致。
Java內部鎖是怎麼實現原子性,有序性,可見性呢?
臨界區中執行操作序列的原子性由鎖的排他性實現。
鎖對可見性的保障是透過寫執行緒和讀執行緒成對地使用儲存屏障和載入屏障實現的。
鎖對有序性的保障是透過寫執行緒和讀執行緒配對使用釋放屏障與獲取屏障屏障實現的。
參考:
《Java多執行緒程式設計實戰指南》黃文海
Volatile的作用是什麼?底層是怎麼實現的?
volatile關鍵字用於修飾共享可變變數,即沒有使用final關鍵字修飾的例項變數或靜態變數,相應的變數就被稱為volatile變數。volatile可以保障可見性、保障有序性和保障long/double型變數讀寫操作的原子性。
可見性和有序性由記憶體屏障保障:
注意:
volatile僅僅保障對其修飾的變數的寫操作(以及讀操作)本身的原子性,而這並不表示對volatile變數的賦值操作一定具有原子性。
volatile關鍵字在可見性方面僅僅是保證讀執行緒能夠讀取到共享變數的相對新值。對於引用型變數和陣列變數,volatile關鍵字並不能保證讀執行緒能夠讀取到相應物件的欄位(例項變數、靜態變數)、元素的相對新值。
參考:
《Java多執行緒程式設計實戰指南》黃文海
Java的CAS是怎麼實現的?Atomic包中的Atmoicinteger和AtmoicintegerFiledUpdater區別?
Java的CAS是怎麼實現的
CAS(CompareandSwap)是對一種處理器指令(例如x86處理器中的cmpxchg指令)的稱呼。
原子變數類(Atomics)通常是藉助一個volatile變數基於CAS實現的能夠保障對共享變數進行read-modify-write更新操作的原子性和可見性的一組工具類。關於CAS的操作都是基於Unsafe類中的一些方法實現的,這些方法對處理器的CAS指令進行了包裝。
簡單分析下Atmoicinteger
主要看下getAndIncrement方法
public final int getAndIncrement() {
return U.getAndAddInt(this, VALUE, 1);
}
這裡的U是private static final jdk.internal.misc.Unsafe U = jdk.internal.misc.Unsafe.getUnsafe();
,是unsafe物件
AtomicInteger中用value來實際儲存值
private static final long VALUE = U.objectFieldOffset(AtomicInteger.class, "value");
,VALUE是value屬性的記憶體地址位置較AtomicInteger物件記憶體地址位置的偏移量,所以U.getAndAddInt(this, VALUE, 1);
中透過this和VALUE可以直接定位到value的記憶體地址位置。
再看getAndAddInt方法
public final int getAndAddInt(Object o, long offset, int delta) {
int v;
do {
v = getIntVolatile(o, offset);
} while (!weakCompareAndSetInt(o, offset, v, v + delta));
return v;
}
首先基於value的記憶體地址位置獲取它的當前值,然後嘗試CAS對value增加delta,如果成功則返回原value值,否則重複以上操作直到成功。
Atomic包中的Atmoicinteger和AtmoicintegerFiledUpdater區別
兩者使用對比:
public class AtomicIntegerFieldUpdaterDemo {
public static class Candidate {
int id;
volatile int score = 0;
AtomicInteger score2 = new AtomicInteger();
}
public static final AtomicIntegerFieldUpdater<Candidate> scoreUpdater =
AtomicIntegerFieldUpdater.newUpdater(Candidate.class, "score");
public static AtomicInteger realScore = new AtomicInteger(0);
public static void main(String[] args) throws InterruptedException {
final Candidate candidate = new Candidate();
Thread[] t = new Thread[10000];
for (int i = 0; i < 10000; i++) {
t[i] = new Thread() {
@Override
public void run() {
if (Math.random() > 0.4) {
candidate.score2.incrementAndGet();
scoreUpdater.incrementAndGet(candidate);
realScore.incrementAndGet();
}
}
};
t[i].start();
}
for (int i = 0; i < 10000; i++) {
t[i].join();
}
System.out.println("AtomicIntegerFieldUpdater Score=" + candidate.score);
System.out.println("AtomicInteger Score=" + candidate.score2.get());
System.out.println("realScore=" + realScore.get());
}
}
輸出臺如下:
AtomicIntegerFieldUpdater Score=6003
AtomicInteger Score=6003
realScore=6003
透過上述程式碼我們不難得知使用AtomicIntegerFieldUpdater與AtomicInteger其實效果是一致的,那既然已經存在了AtomicInteger併發之神又要寫一個AtomicIntegerFieldUpdater呢?
1.從AtomicIntegerFieldUpdaterDemo程式碼中我們不難發現,透過AtomicIntegerFieldUpdater更新score我們獲取最後的int值時相較於AtomicInteger來說不需要呼叫get()方法!
2.對於AtomicIntegerFieldUpdaterDemo類的AtomicIntegerFieldUpdater是static final型別也就是說即使建立了100個物件AtomicIntegerField也只存在一個不會佔用物件的記憶體,但是AtomicInteger會建立多個AtomicInteger物件,佔用的記憶體比AtomicIntegerFieldUpdater大,所以對於熟悉dubbo原始碼的人都知道,dubbo有個實現輪詢負載均衡策略的類AtomicPositiveInteger用的就是AtomicIntegerFieldUpdater。
參考:
《Java多執行緒程式設計實戰指南》黃文海
AtomicIntegerFieldUpdater與AtomicInteger使用引發的思考
sleep和wait的區別?notify和notifyall的區別是啥?
wait\notify\notifyall基本用法
wait模板
// 在呼叫wait方法前獲得相應物件的內部鎖
synchronized(someObject){
while(保護條件不成立){
// 呼叫Object.wait()暫停當前執行緒
someObject.wait();
}
// 程式碼執行到這裡說明保護條件已經滿足
}
// 執行目標動作
doAction();
注意:
保護條件需要迴圈判斷,防止在獲取鎖的時候,保護條件又不成立,內部鎖支援非公平排程,可能有插隊將保護條件給修改了,這是需要讓執行緒重新等待。
notify模板
synchronized(someObject){
updateShareState();
someObject.notify();
}
注意:
notify()需要儘可能放在臨界區的結束的地方,否則被喚醒執行緒可能拿不到鎖(被其他執行緒搶佔),導致上下文切換。
開銷和問題:
Notifyall()過早喚醒,保護條件還沒有成立就被喚醒,可以利用顯示鎖的Condition介面來解決,實現分組喚醒;
notify()和notifyall():
只有在特定條件下采用notify(),否則都用notifyall():
條件1:一次通知只喚醒一個執行緒;條件2:執行緒同質
wait()和sleep()的區別
這兩個方法來自不同的類,sleep()是Thread的靜態方法,wait()是Object的例項方法;
sleep方法沒有釋放鎖,而wait方法釋放了鎖;
wait,notify和notifyAll的呼叫需要放在同一個物件所引導的臨界區中,而sleep可以在任何地方使用;
sleep到時間會自動恢復。wait必須使用notify或者是notifyall進行喚醒;
Wait通常被用於執行緒間互動/通訊,sleep通常被用於暫停執行。
參考:
《Java多執行緒程式設計實戰指南》黃文海
java多執行緒中sleep和wait的4個區別,你知道幾個?
wait()和sleep()方法的區別
await/signal/signalAll/condition
Condition介面可作為wait/notify的替代品來實現等待/通知,它為解決過早喚醒問題提供了支援,並解決了Object.wait(long)不能區分其返回是否是由等待超時而導致的問題。
Lock.newCondition()的返回值就是一個Condition例項,因此呼叫任意一個顯式鎖例項的newCondition方法可以建立一個相應的Condition介面。Object.wait()/notify()要求其執行執行緒持有這些方法所屬物件的內部鎖,類似地,Condition.await()/signal()也要求其執行執行緒持有建立該Condition例項的顯式鎖。
透過condition,可以將等待執行緒分組並進行分組喚醒。
class ConditionUsage {
private final Lock lock = new ReentrantLock();
private final Condition condition = lock.newCondition();
public void aGuaredMethod() throws InterruptedException {
lock.lock();
try {
while (保護條件不滿足) {
condition.await();
}
// 執行目標動作
doAction();
} finally {
lock.unlock();
}
}
private void doAction() {
// ...
}
public void anNotificationMethod() throws InterruptedException {
lock.lock();
try {
// 更新共享變數
changeState();
condition.signal();
} finally {
lock.unlock();
}
}
private void changeState() {
// ...
}
}
Condition.awaitUntil(Date)返回值true表示進行的等待尚未達到最後期限,即此時方法的返回是由於其他執行緒執行了相應條件變數的signal/signalAll方法。
參考:
《Java多執行緒程式設計實戰指南》黃文海
synchronized原理?鎖升級?
JDK1.5之前synchronized是一個重量級鎖,不過,隨著Javs SE 1.6對synchronized進行的各種最佳化後,效能上synchronized和顯示鎖相差不大,不過顯示鎖的功能更為強大,且顯示鎖的讀寫鎖在特定場景效能提升明顯。
synchronized基本原理
synchronized依賴jvm指令實現同步,編譯後可以看到實際靠monitorenter和monitorexit指令來實現同步。底層依賴作業系統的Mutex Lock來實現的。
monitorenter:每個物件都是一個監視器鎖(monitor)。當monitor被佔用時就會處於鎖定狀態,執行緒執行monitorenter指令時嘗試獲取monitor的所有權,過程如下:
1)如果monitor的進入數為0,則該執行緒進入monitor,然後將進入數設定為1,該執行緒即為monitor的所有者,如果同一個執行緒反覆重入則反覆加1;
2)如果執行緒已經佔有該monitor,只是重新進入,則進入monitor的進入數加1;
3)如果其他執行緒已經佔用了monitor,則該執行緒進入阻塞狀態,直到monitor的進入數為0,再重新嘗試獲取monitor的所有權;
monitorexit:執行monitorexit的執行緒必須是objectref所對應的monitor的所有者。指令執行時,monitor的進入數減1,如果減1後進入數為0,那執行緒退出monitor,不再是這個monitor的所有者。其他被這個monitor阻塞的執行緒可以嘗試去獲取這個monitor的所有權。
其實wait/notify等方法也依賴於monitor物件,這就是為什麼只有在同步的塊或者方法中才能呼叫wait/notify等方法,否則會丟擲java.lang.IllegalMonitorStateException的異常的原因。
與一切皆物件一樣,所有的Java物件是天生的Monitor,每一個Java物件都有成為Monitor的潛質。在Java虛擬機器(HotSpot)中,Monitor是由ObjectMonitor實現的。ObjectMonitor中有兩個佇列,_WaitSet 和 _EntryList,用來儲存ObjectWaiter物件列表( 每個等待鎖的執行緒都會被封裝成ObjectWaiter物件 ),_owner指向持有ObjectMonitor物件的執行緒,當多個執行緒同時訪問一段同步程式碼時:
1)首先會進入 _EntryList 集合,當執行緒獲取到物件的monitor後,進入 _Owner區域並把monitor中的owner變數設定為當前執行緒,同時monitor中的計數器count加1;
2)若執行緒呼叫 wait() 方法,將釋放當前持有的monitor,owner變數恢復為null,count自減1,同時該執行緒進入 WaitSet集合中等待被喚醒;
3)若當前執行緒執行完畢,也將釋放monitor(鎖)並復位count的值,以便其他執行緒進入獲取monitor(鎖);
synchronized鎖升級
JDK1.6對synchronized的實現機制進行了較大調整,包括鎖升級、鎖消除、鎖粗化等最佳化策略,使得synchronized效能極大提高。鎖主要存在四種狀態,依次是:無鎖狀態、偏向鎖狀態、輕量級鎖狀態、重量級鎖狀態,鎖升級指鎖可以從偏向鎖升級到輕量級鎖,再升級的重量級鎖。但是鎖的升級是單向的,也就是說只能從低到高升級,不會出現鎖的降級。在JDK1.6中預設是開啟偏向鎖和輕量級鎖的。
偏向鎖
HotSpot作者經過研究實踐發現,在大多數情況下,鎖不僅不存在多執行緒競爭,而且總是由同一執行緒多次獲得。引入偏向鎖是為了在沒有多執行緒競爭的情況下儘量減少不必要的輕量級鎖執行路徑,因為輕量級鎖的加鎖解鎖操作是需要依賴多次CAS原子指令的,而偏向鎖只需要在置換ThreadID的時候依賴一次CAS原子指令。偏向鎖的想法是一旦執行緒第一次獲得了監視物件,之後讓監視物件“偏向”這個執行緒,之後的多次呼叫則可以避免CAS操作,說白了就是置個變數,如果發現為true則無需再走各種加鎖/解鎖流程。
如果一個執行緒A持有偏向鎖,執行緒B來競爭,如果執行緒B透過CAS競爭失敗,則在安全點偏向鎖升級為輕量級鎖。
輕量級鎖
因為重量級鎖會導致執行緒上下文切換,所以偏向鎖不會直接升級到重量級鎖,而是會升級到輕量級鎖的狀態。
在輕量鎖的狀態下,執行緒間透過自旋加CAS的方式來競爭鎖,如果可以很快獲得鎖資源,這樣的開銷比較小,如果自旋一定次數後仍然無法競爭到鎖,此時輕量級鎖升級為重量級鎖。
重量級鎖
Synchronized是透過物件內部的一個叫做監視器鎖(Monitor)來實現的。但是監視器鎖本質又是依賴於底層的作業系統的Mutex Lock來實現的。而作業系統實現執行緒之間的切換這就需要從使用者態轉換到核心態,這個成本非常高,狀態之間的轉換需要相對比較長的時間,這就是為什麼Synchronized效率低的原因。因此,這種依賴於作業系統Mutex Lock所實現的鎖我們稱之為 “重量級鎖”。“重量級鎖”才是我們平時以為的同步鎖。
synchronized其他最佳化
鎖消除
為了保證資料的完整性,在進行操作時需要對這部分操作進行同步控制,但是在有些情況下,JVM檢測到不可能存在共享資料競爭,這是JVM會對這些同步鎖進行鎖消除。
鎖粗化
在使用同步鎖的時候,需要讓同步塊的作用範圍儘可能小—僅在共享資料的實際作用域中才進行同步,這樣做的目的是為了使需要同步的運算元量儘可能縮小,如果存在鎖競爭,那麼等待鎖的執行緒也能儘快拿到鎖。在大多數的情況下,上述觀點是正確的。但是如果一系列的連續加鎖解鎖操作,可能會導致不必要的效能損耗,所以引入鎖粗話的概念。
鎖粗話概念比較好理解,就是將多個連續的加鎖、解鎖操作連線在一起,擴充套件成一個範圍更大的鎖
參考:
《Java多執行緒程式設計實戰指南》黃文海
synchronized原理
AQS
AQS概述
AbstractQueuedSynchronizer(AQS),抽象的佇列式的同步器。AQS維護了一個volatile int state(代表共享資源)和一個FIFO執行緒同步佇列,多執行緒爭用資源被阻塞時執行緒被封裝成Node加入這個佇列中。AQS定義兩種資源共享方式:Exclusive(獨佔,只有一個執行緒能執行,如ReentrantLock)和Share(共享,多個執行緒可同時執行,如Semaphore/CountDownLatch)。
AQS定義了一套多執行緒訪問共享資源的同步器框架,許多同步類實現都依賴於它,如常用的ReentrantLock/Semaphore/CountDownLatch,這些同步類內部都有具體的自定義同步器,這些同步器繼承了AQS,不同的自定義同步器爭用共享資源的方式也不同。自定義同步器在實現時只需要實現共享資源state的獲取與釋放方式即可,至於具體執行緒等待佇列的維護(如獲取資源失敗入隊/喚醒出隊等),AQS已經在頂層實現好了。
自定義同步器實現時主要實現以下幾種方法:
- isHeldExclusively():該執行緒是否正在獨佔資源。只有用到condition才需要去實現它。
- tryAcquire(int):獨佔方式。嘗試獲取資源,成功則返回true,失敗則返回false。
- tryRelease(int):獨佔方式。嘗試釋放資源,成功則返回true,失敗則返回false。
- tryAcquireShared(int):共享方式。嘗試獲取資源。負數表示失敗;0表示成功,但沒有剩餘可用資源;正數表示成功,且有剩餘資源。
- tryReleaseShared(int):共享方式。嘗試釋放資源,如果釋放後允許喚醒後續等待結點返回true,否則返回false。
這些方式不需要都實現,一般實現tryAcquire-tryRelease、tryAcquireShared-tryReleaseShared中的一種即可。
方法詳解
AQS使用時比較核心的方法是acquire-release、acquireShared-releaseShared。
- acquire(int)
ReentrantLock的lock方法,內部其實就是呼叫的acquire(1)
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
tryAcquire()嘗試直接去獲取資源,如果成功則直接返回(這裡體現了非公平鎖,每個執行緒獲取鎖時會嘗試直接搶佔加塞一次,而CLH佇列中可能還有別的執行緒在等待);
如果直接透過tryAcquire()獲取資源失敗,addWaiter()將該執行緒加入等待佇列的尾部,並標記為獨佔模式;
acquireQueued()使執行緒阻塞在等待佇列中獲取資源,一直獲取到資源後才返回。如果在整個等待過程中被中斷過,則返回true,否則返回false;
如果執行緒在等待過程中被中斷過,它是不響應的。只是獲取資源後才再進行自我中斷selfInterrupt(),將中斷補上。
- release(int)
ReentrantLock的unlock方法,內部其實就是呼叫的release(1)
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()釋放資源,自義定同步器在實現時,如果已經徹底釋放資源(state=0),要返回true,否則返回false。
如果資源徹底釋放了(即state=0),unparkSuccessor()會喚醒等待佇列裡的其他執行緒來獲取資源
acquireShared()和releaseShared()類似,不同的是acquireShared()中一個執行緒拿到資源後如果有剩餘資源還會去喚醒後繼執行緒,releaseShared()在釋放部分資源後就會喚醒後繼執行緒,不需要完全釋放資源。
ReentrantLock原理
內部的Sync繼承AQS,而Sync有兩個子類分別為NonfairSync和FairSync,分別對應非公平鎖和公平鎖的Sync,Sync重寫了AQS的tryAcquire和tryRelease等方法。針對公平或不公平的需求,會將Sync設定為NonfairSync或FairSync。
ReentrantLock在呼叫lock和unlock時,實際使用的為Sync(AQS)的acquire(1)和release(1)。
public void lock() {
sync.acquire(1);
}
public void unlock() {
sync.release(1);
}
NonfairSync和FairSync的tryRelease()相同,都是釋放一定的資源量,如果資源為0時,則將獨佔執行緒設為null然後退出。
protected final boolean tryRelease(int releases) {
int c = getState() - releases;
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}
NonfairSync和FairSync的tryAcquire()因為公平性問題,所以有一些差別:NonfairSync的tryAcquire()在資源為0時(c == 0)直接嘗試透過cas來增加state並將當前執行緒設為獨佔執行緒,如果當前執行緒本身就是獨佔執行緒的話,則將state增加一定的數量。
static final class NonfairSync extends Sync {
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
}
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
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;
}
FairSync的tryAcquire()在資源為0時(c == 0)還要看佇列裡是否有執行緒正在排隊(hasQueuedPredecessors()),如果有的話,當前執行緒就得老老實實去排隊,不能插隊,這裡表現了公平性排程。
static final class FairSync extends Sync {
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;
}
}
Condition
AQS中除了剛才說的同步佇列外,還有等待佇列,ReentrantLock中的每個condition都有一個等待佇列,這也是condition實現分組喚醒的基礎。
檢視AQS的newCondition()方法,實際呼叫:
final ConditionObject newCondition() {
return new ConditionObject();
}
直接初始化並返回了一個AQS提供的ConditionObject物件。ConditionObject透過維護firstWaiter和lastWaiter來維護Condition等待佇列。透過signal操作將Condition等待佇列中的執行緒移到Sync鎖等待佇列。
Condition必須與一個獨佔鎖繫結使用,在await或signal之前必須現持有獨佔鎖。Condition佇列是一個單向連結串列,他是公平的,按照先進先出的順序從佇列中被“喚醒”,所謂喚醒指的是完成Condition物件上的等待,被移到Sync鎖等待佇列中,又參與競爭鎖的資格(Sync佇列有公平&非公平兩種模式,注意區別)。
參考:
Java併發之AQS詳解
Java多執行緒之JUC包:Condition原始碼學習筆記