Java可重入鎖ReentrantLock
技術標籤:Java
1. ReentrantLock的概念
ReentrantLock是一個可重入的獨佔鎖(/互斥)鎖。
- 可重入:指任意執行緒在獲取到鎖之後能夠再次獲取該鎖而不會被阻塞。
- 獨佔:每次只能有一個執行緒能持有鎖;與之相應的時共享鎖,則允許多個執行緒同時獲取鎖,併發訪問,共享資源,ReentrantReadWriteLock裡的讀鎖,它的讀鎖是可以被共享的,但是它的寫鎖是獨佔的。
ReentrantLock繼承了Lock介面,其內部類Sync繼承了佇列同步器AQS,Sync有兩個子類:公平鎖FairSync和非公平鎖NonfairSync。在"公平鎖"的機制下,執行緒依次排隊獲取鎖;而"非公平鎖"在鎖是可獲取狀態時,不管自己是否在同步佇列的隊頭都會獲取鎖。"公平鎖"保證了鎖的獲取按照FIFO原則,而代價則是進行大量的執行緒切換,耗時多,開銷大;"非公平鎖"雖然可能導致執行緒飢餓,但卻有極少的執行緒切換,保證了其更大的吞吐量。下面從原始碼的角度重點分析ReentrantLock的可重入、公平鎖和非公平鎖。
2. ReentrantLock的可重入
可重進入是指任意執行緒在獲取到鎖之後能夠再次獲取該鎖而不會被鎖阻塞,該特性的實現需要解決以下兩個問題:
執行緒再次獲取鎖:鎖需要去識別獲取鎖的執行緒是否為當前佔據鎖的執行緒,如果是,則再次成功獲取,同步狀態自增;
鎖的最終釋放:鎖釋放時,同步狀態自減,執行緒重複 n 次獲取了鎖,需要進行 n 次釋放鎖,當同步狀態等於 0時,鎖釋放成功,其它執行緒才能夠獲取到該鎖。
所以只要分析清楚了ReentrantLock獲取鎖和釋放鎖的原理,就分析清楚了可重入。
3. 獲取鎖
獲取鎖和釋放鎖的過程,其實就是獲取同步狀態和釋放同步狀態的過程。子類繼承AQS之後,需要重寫tryAcquire()
tryRelease()
、tryAcquireShared()
、tryReleaseShared()
和isHeldExclusively()
方法,它們的功能分別是 以獨佔方式獲取鎖、以獨佔釋放鎖、以共享方式獲取鎖、以獨佔方式釋放鎖 和 判斷同步狀態是否被當前執行緒獨佔。ReentrantLcok是獨佔鎖,所以只需要重寫tryAcquire()
、tryRelease()
和isHeldExclusively()
方法即可。
ReentrantLock分為公平鎖和非公平鎖,它們對獲取鎖的方式不一樣,也就是tryAcquire()
方法的實現不一樣,所以公平鎖類FairSync和非公平鎖NonfairSync要分別重寫該方法;而它們釋放鎖的方式一樣,所以tryRelease()方
isHeldExclusively()
方法也在Sync中重寫。
3.1 公平鎖
佇列同步器AQS的內部維持著一個FIFO雙向等待佇列,公平鎖的獲取順序符合FIFO原則,如果當前同步狀態為0,需要呼叫hasQueuedPredecessors()
方法判斷同步佇列中當前節點是否有前驅節點;如果當前同步狀態不為0,需要判斷已獲取鎖的執行緒是否為當前執行緒,如果是,則同步狀態自增。公平鎖FairSync的tryAcquire(int)
方法如下所示:
/**
* 以公平方式獲取鎖
*/
protected final boolean tryAcquire(int acquires) {
// 獲取當前執行緒
final Thread current = Thread.currentThread();
// 獲取同步狀態
int c = getState();
// 同步狀態為0,表示沒有執行緒獲取鎖
if (c == 0) {
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
// 同步狀態不為0,表示已經有執行緒獲取了鎖,判斷獲取鎖的執行緒是否為當前執行緒
else if (current == getExclusiveOwnerThread()) {
// 獲取鎖的執行緒是當前執行緒
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
hasQueuedPredecessors()
方法主要是對同步佇列中當前節點是否有前驅節點進行判斷,如果該方法返回true,則表示有執行緒比當前執行緒更早地請求獲取鎖,因此需要等待前驅執行緒獲取並釋放鎖之後才能繼續獲取鎖,其原始碼如下所示:
public final boolean hasQueuedPredecessors() {
// The correctness of this depends on head being initialized
// before tail and on head.next being accurate if the current
// thread is first in queue.
// 同步佇列尾節點
Node t = tail; // Read fields in reverse initialization order
// 同步佇列頭節點
Node h = head;
Node s;
return h != t &&
((s = h.next) == null || s.thread != Thread.currentThread());
}
3.2 非公平鎖
非公平鎖的實現在Sync的nonfairTryAcquire()
方法中,與公平鎖比較,唯一不同就是非公平鎖不需要進行hasQueuedPredecessors()
判斷,原始碼如下圖所示:
/**
* 以非公平方式獲取鎖
*/
final boolean nonfairTryAcquire(int acquires) {
// 獲取當前執行緒
final Thread current = Thread.currentThread();
// 獲取同步狀態
int c = getState();
// 同步狀態為0,表示沒有執行緒獲取鎖
if (c == 0) {
// 執行CAS操作,嘗試修改同步狀態
if (compareAndSetState(0, acquires)) {
// 同步狀態修改成功,獲取到鎖
setExclusiveOwnerThread(current);
return true;
}
}
// 同步狀態不為0,表示已經有執行緒獲取了鎖,判斷獲取鎖的執行緒是否為當前執行緒
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;
}
4. 釋放鎖
公平鎖和非公平鎖的釋放鎖的方法tryRelease()
都自繼承父類Sync,也就是tryRelease()
方法在Sync重寫。獲取鎖時,同步狀態自增;釋放鎖時,同步狀態自減;ReentrantLock是可重入鎖,如果執行緒重複獲取 n 次鎖,就需要釋放 n 次鎖,即同步狀態為0,這樣釋放鎖才成功,其它等待的執行緒才可以獲取同步狀態,tryRelease()
原始碼如下:
protected final boolean tryRelease(int releases) {
// 計算新的狀態值
int c = getState() - releases;
// 判斷當前執行緒是否是持有鎖的執行緒,如果不是的話,丟擲異常
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
// 新的狀態值是否為0,若為0,則表示該鎖已經完全釋放了,其他執行緒可以獲取同步狀態了
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
// 更新狀態值
setState(c);
return free;
}
5. ReentrantLock與synchronized的區別與聯絡
區別:
- ReentrantLock是JDK類層面實現;synchronized是JVM層面實現。
- ReentrantLock增加了一些高階功能,主要以下三項:等待可中斷、可實現公平鎖及可以繫結多個條件(一個ReentrantLock物件可以同時繫結多個Condition物件)。
聯絡:
- ReentrantLock與synchronized都是可重入鎖,同一執行緒反覆進入同步塊也不會出現自己把自己鎖死的情況。
JDK 6或以上版本,效能已經不再是選擇synchronized或者ReentrantLock的決定因素。基於以下理由,我們仍然推薦在synchronized與ReentrantLock都可滿足需要時優先使用synchronized:
- synchronized是在Java語法層面的同步,足夠清晰,也足夠簡單。每個Java程式設計師都熟悉synchronized,但J.U.C中的Lock介面則並非如此。因此在只需要基礎的同步功能時,更推薦synchronized。
- Lock應該確保在finally塊中釋放鎖,否則一旦受同步保護的程式碼塊中丟擲異常,則有可能永遠不會釋放持有的鎖。這一點必須由程式設計師自己來保證,而使用synchronized的話則可以由Java虛擬機器來確保即使出現異常,鎖也能被自動釋放。
- 儘管在JDK 5時代ReentrantLock曾經在效能上領先過synchronized,但這已經是十多年之前的勝利了。從長遠來看,Java虛擬機器更容易針對synchronized來進行優化,因為Java虛擬機器可以線上程和物件的元資料中記錄synchronized中鎖的相關資訊,而使用J.U.C中的Lock的話,Java虛擬機器是很難得知具體哪些鎖物件是由特定執行緒鎖持有的。