J.U.C之重入鎖:ReentrantLock
此篇部落格所有原始碼均來自JDK 1.8
ReentrantLock,可重入鎖,是一種遞迴無阻塞的同步機制。它可以等同於synchronized的使用,但是ReentrantLock提供了比synchronized更強大、靈活的鎖機制,可以減少死鎖發生的概率。
API介紹如下:
一個可重入的互斥鎖定 Lock,它具有與使用 synchronized 方法和語句所訪問的隱式監視器鎖定相同的一些基本行為和語義,但功能更強大。ReentrantLock 將由最近成功獲得鎖定,並且還沒有釋放該鎖定的執行緒所擁有。當鎖定沒有被另一個執行緒所擁有時,呼叫 lock 的執行緒將成功獲取該鎖定並返回。如果當前執行緒已經擁有該鎖定,此方法將立即返回。可以使用 isHeldByCurrentThread() 和 getHoldCount() 方法來檢查此情況是否發生。
ReentrantLock還提供了公平鎖也非公平鎖的選擇,構造方法接受一個可選的公平引數(預設非公平鎖),當設定為true時,表示公平鎖,否則為非公平鎖。公平鎖與非公平鎖的區別在於公平鎖的鎖獲取是有順序的。但是公平鎖的效率往往沒有非公平鎖的效率高,在許多執行緒訪問的情況下,公平鎖表現出較低的吞吐量。
獲取鎖
我們一般都是這麼使用ReentrantLock獲取鎖的:
//非公平鎖
ReentrantLock lock = new ReentrantLock();
lock.lock();
lock方法:
public void lock() { sync.lock(); }
Sync為ReentrantLock裡面的一個內部類,它繼承AQS(AbstractQueuedSynchronizer),它有兩個子類:公平鎖FairSync和非公平鎖NonfairSync。
ReentrantLock裡面大部分的功能都是委託給Sync來實現的,同時Sync內部定義了lock()抽象方法由其子類去實現,預設實現了nonfairTryAcquire(int acquires)方法,可以看出它是非公平鎖的預設實現方式。下面我們看非公平鎖的lock()方法:
final void lock() { //嘗試獲取鎖 if (compareAndSetState(0, 1)) setExclusiveOwnerThread(Thread.currentThread()); else //獲取失敗,呼叫AQS的acquire(int arg)方法 acquire(1); }
首先會第一次嘗試快速獲取鎖,如果獲取失敗,則呼叫acquire(int arg)方法,該方法定義在AQS中,如下:
public final void acquire(int arg) {
if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
這個方法首先呼叫tryAcquire(int arg)方法,在AQS中講述過,tryAcquire(int arg)需要自定義同步元件提供實現,非公平鎖實現如下:
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
final boolean nonfairTryAcquire(int acquires) {
//當前執行緒
final Thread current = Thread.currentThread();
//獲取同步狀態
int c = getState();
//state == 0,表示沒有該鎖處於空閒狀態
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;
}
該方法主要邏輯:首先判斷同步狀態state == 0 ?,如果是表示該鎖還沒有被執行緒持有,直接通過CAS獲取同步狀態,如果成功返回true。如果state != 0,則判斷當前執行緒是否為獲取鎖的執行緒,如果是則獲取鎖,成功返回true。成功獲取鎖的執行緒再次獲取鎖,這是增加了同步狀態state。
釋放鎖
獲取同步鎖後,使用完畢則需要釋放鎖,ReentrantLock提供了unlock釋放鎖:
public void unlock() {
sync.release(1);
}
unlock內部使用Sync的release(int arg)釋放鎖,release(int arg)是在AQS中定義的:
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0) unparkSuccessor(h);
return true;
}
return false;
}
與獲取同步狀態的acquire(int arg)方法相似,釋放同步狀態的tryRelease(int arg)同樣是需要自定義同步元件自己實現:
protected final boolean tryRelease(int releases) {
//減掉releases
int c = getState() - releases;
//如果釋放的不是持有鎖的執行緒,丟擲異常
if (Thread.currentThread() != getExclusiveOwnerThread()) throw new IllegalMonitorStateException();
boolean free = false; //state == 0 表示已經釋放完全了,其他執行緒可以獲取同步狀態了
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}
只有當同步狀態徹底釋放後該方法才會返回true。當state == 0 時,則將鎖持有執行緒設定為null,free= true,表示釋放成功。
公平鎖與非公平鎖
公平鎖與非公平鎖的區別在於獲取鎖的時候是否按照FIFO的順序來。釋放鎖不存在公平性和非公平性,上面以非公平鎖為例,下面我們來看看公平鎖的tryAcquire(int arg):
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;
}
比較非公平鎖和公平鎖獲取同步狀態的過程,會發現兩者唯一的區別就在於公平鎖在獲取同步狀態時多了一個限制條件:hasQueuedPredecessors(),定義如下:
public final boolean hasQueuedPredecessors() {
Node t = tail;
//尾節點
Node h = head;
//頭節點
Node s;
//頭節點 != 尾節點
// 同步佇列第一個節點不為null
// 當前執行緒是同步佇列第一個節點
return h != t && ((s = h.next) == null || s.thread != Thread.currentThread());
}
該方法主要做一件事情:主要是判斷當前執行緒是否位於CLH同步佇列中的第一個。如果是則返回true,否則返回false。
ReentrantLock與synchronized的區別
前面提到ReentrantLock提供了比synchronized更加靈活和強大的鎖機制,那麼它的靈活和強大之處在哪裡呢?他們之間又有什麼相異之處呢?
首先他們肯定具有相同的功能和記憶體語義。
- 與synchronized相比,ReentrantLock提供了更多,更加全面的功能,具備更強的擴充套件性。例如:時間鎖等候,可中斷鎖等候,鎖投票。
- ReentrantLock還提供了條件Condition,對執行緒的等待、喚醒操作更加詳細和靈活,所以在多個條件變數和高度競爭鎖的地方,ReentrantLock更加適合(以後會闡述Condition)。
- ReentrantLock提供了可輪詢的鎖請求。它會嘗試著去獲取鎖,如果成功則繼續,否則可以等到下次執行時處理,而synchronized則一旦進入鎖請求要麼成功要麼阻塞,所以相比synchronized而言,ReentrantLock會不容易產生死鎖些。
- ReentrantLock支援更加靈活的同步程式碼塊,但是使用synchronized時,只能在同一個synchronized塊結構中獲取和釋放。注:ReentrantLock的鎖釋放一定要在finally中處理,否則可能會產生嚴重的後果。
- ReentrantLock支援中斷處理,且效能較synchronized會好些。
參考資料
- Doug Lea:《Java併發程式設計實戰》
- 方騰飛:《Java併發程式設計的藝術》