ReentrantLock與synchronized 原始碼解析
一.概念及執行原理
在 JDK 1.5 之前共享物件的協調機制只有 synchronized 和 volatile,在 JDK 1.5 中增加了新的機制 ReentrantLock,該機制的誕生並不是為了替代 synchronized,而是在 synchronized 不適用的情況下,提供一種可以選擇的高階功能。
二.synchronized 和 ReentrantLock 的實現和區別
1.實現的方式
synchronized
- synchronized 屬於獨佔式悲觀鎖,是通過 JVM 隱式實現的
- synchronized 只允許同一時刻只有一個執行緒操作資源。
在 Java 中每個物件都隱式包含一個 monitor(監視器)物件,加鎖的過程其實就是競爭 monitor 的過程,當執行緒進入位元組碼 monitorenter 指令之後,執行緒將持有 monitor 物件,執行 monitorexit 時釋放 monitor 物件,當其他執行緒沒有拿到 monitor 物件時,則需要阻塞等待獲取該物件。
ReentrantLock
- ReentrantLock 是 Lock 的預設實現方式之一,它是基於 AQS(Abstract Queued Synchronizer,佇列同步器)實現的
- 它預設是通過非公平鎖實現的,在它的內部有一個 state 的狀態欄位用於表示鎖是否被佔用,如果是 0 則表示鎖未被佔用,此時執行緒就可以把 state 改為 1,併成功獲得鎖,而其他未獲得鎖的執行緒只能去排隊等待獲取鎖資源。
2.區別
(1)效能
synchronized 和 ReentrantLock 都提供了鎖的功能,具備互斥性和不可見性。在 JDK 1.5 中 synchronized 的效能遠遠低於 ReentrantLock,但在 JDK 1.6 之後 synchronized 的效能略低於 ReentrantLock
(2)實現方式
- synchronized 是 JVM 隱式實現的、
- ReentrantLock 是 Java 語言提供的 API;
ReentrantLock 可設定為公平鎖,而 synchronized 卻不行;
(3)作用域
- ReentrantLock 只能修飾程式碼塊
- synchronized 可以用於修飾方法、修飾程式碼塊等;
(4)鎖的釋放
- ReentrantLock 需要手動加鎖和釋放鎖,如果忘記釋放鎖,則會造成資源被永久佔用
- synchronized 無需手動釋放鎖;
ReentrantLock 可以知道是否成功獲得了鎖,而 synchronized 卻不行。
三.ReentrantLock 原始碼分析(ReentrantLock 的具體實現細節)
1.ReentrantLock 的兩個建構函式
首先來看 ReentrantLock 的兩個建構函式:
public ReentrantLock() {
sync = new NonfairSync(); // 非公平鎖
}
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
無參的建構函式建立了一個非公平鎖,使用者也可以根據第二個建構函式,設定一個 boolean 型別的值,來決定是否使用公平鎖來實現執行緒的排程。
2.公平鎖 VS 非公平鎖
公平鎖
公平鎖執行緒需要按照請求的順序來獲得鎖
非公平鎖
非公平鎖允許“插隊”的情況存在,所謂的“插隊”指的是,執行緒在傳送請求的同時該鎖的狀態恰好變成了可用,那麼此執行緒就可以跳過佇列中所有排隊的執行緒直接擁有該鎖。
3.ReentrantLock 非公平鎖原始碼
公平鎖由於有掛起和恢復所以存在一定的開銷,因此效能不如非公平鎖,所以 ReentrantLock 和 synchronized 預設都是非公平鎖的實現方式。
3.1 ReentrantLock的加鎖和解鎖方法
ReentrantLock 是通過 lock() 來獲取鎖,並通過 unlock() 釋放鎖,使用程式碼如下:
Lock lock = new ReentrantLock();
try {
// 加鎖
lock.lock();
//......業務處理
} finally {
// 釋放鎖
lock.unlock();
}
3.2 加鎖的流程
ReentrantLock 中的 lock() 是通過 sync.lock()實現 的,但 Sync 類中的 lock() 是一個抽象方法,需要子類 NonfairSync 或 FairSync 去實現,
3.2.1 NonfairSync 中的 lock() 原始碼
final void lock() {
if (compareAndSetState(0, 1))
// 將當前執行緒設定為此鎖的持有者
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
3.2.2 FairSync 中的 lock() 原始碼
final void lock() {
acquire(1);
}
可以看出非公平鎖比公平鎖只是多了一行 compareAndSetState 方法,該方法是嘗試將 state 值由 0 置換為 1,如果設定成功的話,則說明當前沒有其他執行緒持有該鎖,不用再去排隊了,可直接佔用該鎖,否則,則需要通過 acquire 方法去排隊。
3.2.3 acquire 原始碼
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
3.2.4 tryAcquire 原始碼
tryAcquire 方法嘗試獲取鎖,如果獲取鎖失敗,則把它加入到阻塞佇列中,來看 tryAcquire 的原始碼:
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
// 公平鎖比非公平鎖多了一行程式碼 !hasQueuedPredecessors()
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); // set state=state+1
return true;
}
return false;
}
對於此方法來說,公平鎖比非公平鎖只多一行程式碼 !hasQueuedPredecessors(),它用來檢視佇列中是否有比它等待時間更久的執行緒,如果沒有,就嘗試一下是否能獲取到鎖,如果獲取成功,則標記為已經被佔用。
如果獲取鎖失敗,則呼叫 addWaiter 方法把執行緒包裝成 Node 物件,同時放入到佇列中,但 addWaiter 方法並不會嘗試獲取鎖,acquireQueued 方法才會嘗試獲取鎖,如果獲取失敗,則此節點會被掛起,
3.2.5 acquireQueued 原始碼
/**
* 佇列中的執行緒嘗試獲取鎖,失敗則會被掛起
*/
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); // 獲取成功,將當前節點設定為 head 節點
p.next = null; // 原 head 節點出隊,等待被 GC
failed = false; // 獲取成功
return interrupted;
}
// 判斷獲取鎖失敗後是否可以掛起
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
// 執行緒若被中斷,返回 true
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
該方法會使用 for(;