我們在之前介紹了併發程式設計的鎖機制:synchronized和lock,lock介面的重要實現類是可重入鎖ReentrantLock
。而上一篇併發Lock之AQS(AbstractQueuedSynchronizer)詳解介紹了AQS,談到ReentrantLock,不得不談抽象類AbstractQueuedSynchronizer(AQS)。AQS定義了一套多執行緒訪問共享資源的同步器框架,ReentrantLock的實現依賴於該同步器。本文在介紹過AQS,結合其具體的實現類ReentrantLock
分析實現原理。
ReentrantLock類圖
ReentrantLock實現了Lock介面,內部有三個內部類,Sync、NonfairSync、FairSync,Sync是一個抽象型別,它繼承AbstractQueuedSynchronizer,這個AbstractQueuedSynchronizer是一個模板類,它實現了許多和鎖相關的功能,並提供了鉤子方法供使用者實現,比如tryAcquire,tryRelease等。Sync實現了AbstractQueuedSynchronizer的tryRelease方法。NonfairSync和FairSync兩個類繼承自Sync,實現了lock方法,公平搶佔和非公平搶佔針對tryAcquire有不同的實現。本文重點介紹ReentrantLock預設的實現,即非公平鎖的獲取鎖和釋放鎖的實現。
非公平鎖的lock方法
lock方法
- 在初始化ReentrantLock的時候,如果我們不傳引數,那麼預設使用非公平鎖,也就是NonfairSync。
public ReentrantLock() {
sync = new NonfairSync();
}
複製程式碼
- 當我們呼叫ReentrantLock的lock方法的時候,實際上是呼叫了NonfairSync的lock方法,這個方法先用CAS操作,去嘗試搶佔該鎖。如果成功,就把當前執行緒設定在這個鎖上,表示搶佔成功。如果失敗,則呼叫acquire模板方法,等待搶佔。程式碼如下:
static final class NonfairSync extends Sync {
final void lock() {
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
}
複製程式碼
- 上面呼叫
acquire(1)
實際上使用的是AbstractQueuedSynchronizer
的acquire方法,它是一套鎖搶佔的模板,總體原理是先去嘗試獲取鎖,如果沒有獲取成功,就在CLH佇列中增加一個當前執行緒的節點,表示等待搶佔。然後進入CLH佇列的搶佔模式,進入的時候也會去執行一次獲取鎖的操作,如果還是獲取不到,就呼叫LockSupport.park將當前執行緒掛起。那麼當前執行緒什麼時候會被喚醒呢?當持有鎖的那個執行緒呼叫unlock的時候,會將CLH佇列的頭節點的下一個節點上的執行緒喚醒,呼叫的是LockSupport.unpark方法。acquire程式碼比較簡單,具體如下:
public final void acquire(int arg) {
if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
複製程式碼
- acquire方法內部先使用tryAcquire這個鉤子方法去嘗試再次獲取鎖,這個方法在NonfairSync這個類中其實就是使用了nonfairTryAcquire,具體實現原理是先比較當前鎖的狀態是否是0,如果是0,則嘗試去原子搶佔這個鎖(設定狀態為1,然後把當前執行緒設定成獨佔執行緒),如果當前鎖的狀態不是0,就去比較當前執行緒和佔用鎖的執行緒是不是一個執行緒,如果是,會去增加狀態變數的值,從這裡看出可重入鎖之所以可重入,就是同一個執行緒可以反覆使用它佔用的鎖。如果以上兩種情況都不通過,則返回失敗false。程式碼如下:
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;
}
複製程式碼
- tryAcquire一旦返回false,就會則進入acquireQueued流程,也就是基於CLH佇列的搶佔模式
首先,在CLH鎖佇列尾部增加一個等待節點,這個節點儲存了當前執行緒,通過呼叫addWaiter實現,這裡需要考慮初始化的情況,在第一個等待節點進入的時候,需要初始化一個頭節點然後把當前節點加入到尾部,後續則直接在尾部加入節點就行了。
private Node addWaiter(Node mode) {
// 初始化一個節點,這個節點儲存當前執行緒
Node node = new Node(Thread.currentThread(), mode);
// 當CLH佇列不為空的視乎,直接在佇列尾部插入一個節點
Node pred = tail;
if (pred != null) {
node.prev = pred;
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
// 當CLH佇列為空的時候,呼叫enq方法初始化佇列
enq(node);
return node;
}
private Node enq(final Node node) {
for (;;) {
Node t = tail;
if (t == null) { // 初始化節點,頭尾都指向一個空節點
if (compareAndSetHead(new Node()))
tail = head;
} else {// 考慮併發初始化
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
複製程式碼
- 將節點增加到CLH佇列後,進入acquireQueued方法。
首先,外層是一個無限for迴圈,如果當前節點是頭節點的下個節點,並且通過tryAcquire獲取到了鎖,說明頭節點已經釋放了鎖,當前執行緒是被頭節點那個執行緒喚醒的,這時候就可以將當前節點設定成頭節點,並且將failed標記設定成false,然後返回。至於上一個節點,它的next變數被設定為null,在下次GC的時候會清理掉。
如果本次迴圈沒有獲取到鎖,就進入執行緒掛起階段,也就是shouldParkAfterFailedAcquire這個方法。
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);
}
}
複製程式碼
- 如果嘗試獲取鎖失敗,就會進入shouldParkAfterFailedAcquire方法,會判斷當前執行緒是否掛起,如果前一個節點已經是SIGNAL狀態,則當前執行緒需要掛起。如果前一個節點是取消狀態,則需要將取消節點從佇列移除。如果前一個節點狀態是其他狀態,則嘗試設定成SIGNAL狀態,並返回不需要掛起,從而進行第二次搶佔。完成上面的事後進入掛起階段。
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus;
if (ws == Node.SIGNAL)
//
return true;
if (ws > 0) {
//
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
//
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
複製程式碼
- 當進入掛起階段,會進入parkAndCheckInterrupt方法,則會呼叫LockSupport.park(this)將當前執行緒掛起。
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);
return Thread.interrupted();
}
複製程式碼
非公平鎖的unlock方法
- 呼叫unlock方法,其實是直接呼叫AbstractQueuedSynchronizer的release操作。
public void unlock() {
sync.release(1);
}
複製程式碼
- 進入release方法,內部先嚐試tryRelease操作,主要是去除鎖的獨佔執行緒,然後將狀態減一,這裡減一主要是考慮到可重入鎖可能自身會多次佔用鎖,只有當狀態變成0,才表示完全釋放了鎖。
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成功,則將CHL佇列的頭節點的狀態設定為0,然後喚醒下一個非取消的節點執行緒。
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;
}
複製程式碼
- 一旦下一個節點的執行緒被喚醒,被喚醒的執行緒就會進入acquireQueued程式碼流程中,去獲取鎖。
private void unparkSuccessor(Node node) {
int ws = node.waitStatus;
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
Node s = node.next;
if (s == null || s.waitStatus > 0) {
s = null;
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)
s = t;
}
if (s != null)
LockSupport.unpark(s.thread);
}
複製程式碼
tryLock
在ReetrantLock的tryLock(long timeout, TimeUnit unit)
提供了超時獲取鎖的功能。它的語義是在指定的時間內如果獲取到鎖就返回true,獲取不到則返回false。這種機制避免了執行緒無限期的等待鎖釋放。
public boolean tryLock(long timeout, TimeUnit unit)
throws InterruptedException {
return sync.tryAcquireNanos(1, unit.toNanos(timeout));
}
複製程式碼
具體看一下內部類裡面的方法tryAcquireNanos
:
public final boolean tryAcquireNanos(int arg, long nanosTimeout)
throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
return tryAcquire(arg) ||
doAcquireNanos(arg, nanosTimeout);
}
複製程式碼
如果執行緒被中斷了,那麼直接丟擲InterruptedException。如果未中斷,先嚐試獲取鎖,獲取成功就直接返回,獲取失敗則進入doAcquireNanos。tryAcquire我們已經看過,這裡重點看一下doAcquireNanos做了什麼。
private boolean doAcquireNanos(int arg, long nanosTimeout)
throws InterruptedException {
// 起始時間
long lastTime = System.nanoTime();
// 執行緒入隊
final Node node = addWaiter(Node.EXCLUSIVE);
boolean failed = true;
try {
// 又是自旋!
for (;;) {
// 獲取前驅節點
final Node p = node.predecessor();
// 如果前驅是頭節點並且佔用鎖成功,則將當前節點變成頭結點
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return true;
}
// 如果已經超時,返回false
if (nanosTimeout <= 0)
return false;
// 超時時間未到,且需要掛起
if (shouldParkAfterFailedAcquire(p, node) &&
nanosTimeout > spinForTimeoutThreshold)
// 阻塞當前執行緒直到超時時間到期
LockSupport.parkNanos(this, nanosTimeout);
long now = System.nanoTime();
// 更新nanosTimeout
nanosTimeout -= now - lastTime;
lastTime = now;
if (Thread.interrupted())
//相應中斷
throw new InterruptedException();
}
} finally {
if (failed)
cancelAcquire(node);
}
}
複製程式碼
doAcquireNanos的流程簡述為:執行緒先入等待佇列,然後開始自旋,嘗試獲取鎖,獲取成功就返回,失敗則在佇列裡找一個安全點把自己掛起直到超時時間過期。這裡為什麼還需要迴圈呢?因為當前執行緒節點的前驅狀態可能不是SIGNAL,那麼在當前這一輪迴圈中執行緒不會被掛起,然後更新超時時間,開始新一輪的嘗試。
總結
ReentrantLock是可重入的鎖,其內部使用的就是獨佔模式的AQS。公平鎖和非公平鎖不同之處在於,公平鎖在獲取鎖的時候,不會先去檢查state狀態,而是直接執行aqcuire(1)。公平鎖多了hasQueuePredecessors這個方法,這個方法用於判斷CHL佇列中是否有節點,對於公平鎖,如果CHL佇列有節點,則新進入競爭的執行緒一定要在CHL上排隊,而非公平鎖則是無視CHL佇列中的節點,直接進行競爭搶佔,這就有可能導致CHL佇列上的節點永遠獲取不到鎖,這就是非公平鎖之所以不公平的原因,這裡不再贅述。