併發Lock之ReentrantLock實現原理

aoho發表於2019-03-03

我們在之前介紹了併發程式設計的鎖機制:synchronized和lock,lock介面的重要實現類是可重入鎖ReentrantLock。而上一篇併發Lock之AQS(AbstractQueuedSynchronizer)詳解介紹了AQS,談到ReentrantLock,不得不談抽象類AbstractQueuedSynchronizer(AQS)。AQS定義了一套多執行緒訪問共享資源的同步器框架,ReentrantLock的實現依賴於該同步器。本文在介紹過AQS,結合其具體的實現類ReentrantLock分析實現原理。

ReentrantLock類圖

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佇列上的節點永遠獲取不到鎖,這就是非公平鎖之所以不公平的原因,這裡不再贅述。

訂閱最新文章,歡迎關注我的公眾號

微信公眾號

參考

  1. Java中可重入鎖ReentrantLock原理剖析
  2. ReentrantLock實現原理

相關文章