不得不知的ReentrantLock原始碼

EumJi發表於2018-02-11

本文首發於www.eumji025.com/2018/02/11/…

前言

處理好執行緒之間的同步問題一直都是開發界的難題,我們最常用的就是synchronized關鍵字,synchronized常用在方法上或者使用synchronized塊.

而且synchronized在JDK1.6之後得到了很多效能上的改進,,當然平常我們也鼓勵儘量多使用synchronized關鍵字,因為非常的簡單,遮蔽了很多實現上和操作上的細節。

但是我不得不說作為開發人員的我們,如果想要最大化的優化或者靈活的進行控制,還是要對另外一種同步方式要有一定的瞭解。那就是我們今天所要討論的重點物件lock

為什麼會誕生lock,我想我們也是很容理解的,因為JDK1.6之前的synchronized關鍵字不夠高效,而且synchronized不夠靈活(比如無法使用嘗試在規定時間內獲取鎖)等,所以就誕生了lock.

lock改善了很多同步上的效能問題,而且有非常靈活的API.

今天我們就ReentrantLock類(一種獨佔式的鎖)開始我們的話題.

ReentrantLock

ReentrantLock是通過擴充套件AbstractQueuedSynchronizer進行鎖的控制,幷包含公平鎖和非公平鎖,此處說的公平鎖和非公平鎖的最大區別就體現在嘗試獲取鎖的過程中,非公平鎖立即就可以進行嘗試獲取鎖,而公平鎖不可以直接嘗試獲取鎖,必須按順序進入等待鎖的佇列,下面詳細通過程式碼詳細介紹一下.

首先看一下我們的程式碼結構圖

不得不知的ReentrantLock原始碼

從上圖我們可以看到ReentrantLock存在三個內部類,請記住這三個內部類,Sysc繼承了AbstractQueuedSynchronizer這個類,而NonfairSyncFairSync又繼承了Sync類,這兩個類分別是非公平和公平鎖實現的關鍵.

下面看一下公平鎖和非公平鎖如何宣告的

//非公平鎖
public ReentrantLock() {
    sync = new NonfairSync();
}
public ReentrantLock(boolean fair) {
    sync = fair ? new FairSync() : new NonfairSync();
}
複製程式碼

預設情況下我們的宣告的都是非公平鎖,如果需要宣告為公平鎖則可以自己通過fair變數來設定.

獲取鎖

下面我們看一下具體的差別

//NonfairSync獲取lock
final void lock() {
  if (compareAndSetState(0, 1))
     setExclusiveOwnerThread(Thread.currentThread());
  else
     acquire(1);
}
//fairSync獲取lock
final void lock() {
    acquire(1);
}
複製程式碼

就像我們在上面說的那樣,對於非公平鎖首先會嘗試獲取一次鎖,如果成功獲取鎖就設定當前的獨佔鎖為當前執行緒,並改變狀態值為1.否則都進行acquire方法.

下面看一下acquire方法的具體實現

public final void acquire(int arg) {
   if (!tryAcquire(arg) &&
       acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
       selfInterrupt();
}
複製程式碼

這裡主要做了幾個事情:

1.嘗試獲取鎖或者改變鎖的數量,如果操作失敗

2.如果沒有獲取到,則把當前執行緒加入到等待的連結串列

3.如果新增成功則設定執行緒中斷狀態為true

下面先看一下tryAcquire方法的主要內容,首先看一下非公平鎖的實現

final boolean nonfairTryAcquire(int acquires) {
   //獲取當前你執行緒
    final Thread current = Thread.currentThread();
   //獲取當前的狀態值
    int c = getState();
   //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;
    }
  //否則就設定為false
    return false;
}
複製程式碼

上面介紹了非公平鎖的方法,其實實現很簡單,首先判斷是否當前時刻沒有執行緒獲得鎖,如果沒有則自己嘗試獲取鎖,如果當前已經有執行緒獲得了鎖,那麼就需要判斷獲得鎖的是不是就是當前執行緒,如果是的話代表是重入鎖,將獲得鎖的數量加1。

然後我們在看一下公平鎖的內部實現有什麼不同

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;
        }
    }
    ...省略
    return false;
}
複製程式碼

其實公平鎖差不多,只是在判斷時多加了hasQueuedPredecessors方法判斷當前執行緒是否為列表頭,不為列表頭且也不是表頭的下一個元素,則不能獲得了鎖。

入隊自旋

當上面都沒有成功的情況下,說明已經有執行緒獲得了鎖,那麼我們需要把當前的執行緒加入到等待的佇列中.具體的就不贅述了,已經在我的另外一篇文章併發 - AbstractQueuedSynchronizer分析中有講到底是如何進入等待佇列和進行自旋的,本文主要講述ReentrantLock的相關部分.

unlock方法

unlock是當前執行緒釋放鎖的方法,是通過呼叫release方法實現的

public void unlock() {
    sync.release(1);
}
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是由sync類重寫的,FairSync和NonfairSync類都沒有重寫,因為釋放的原理都是一樣的,下面看一下具體的實現

protected final boolean tryRelease(int releases) {
   //狀態值相減,因為有重入鎖的情況
    int c = getState() - releases;
   //如果當前執行緒並沒有持有鎖,拋異常  
  if (Thread.currentThread() != getExclusiveOwnerThread())
        throw new IllegalMonitorStateException();
    boolean free = false;
   //等於0,說明完全釋放,不然只是釋放了一層鎖  
  if (c == 0) {
        free = true;
        setExclusiveOwnerThread(null);
    }
  //否則只是更新狀態值
    setState(c);
    return free;
}
複製程式碼

道理很簡單就是:判斷當前執行緒獲取是否獲得了鎖,如果是就減去releases個鎖,如果狀態值為0則表示已釋放完,否則沒有獲得鎖的執行緒呼叫釋放鎖的方法會丟擲異常.

unparkSuccessor也是父類AQS中的方法,主要做的事情就是解鎖離head最近的那個正在等待執行緒.為什麼這麼說是因為有可能head.next已經取消了。

tryLock

下面我們在看一下擴充套件的方法tryLock,此方法是為了執行緒漫長等待鎖的問題,意在嘗試性的獲取鎖,如果沒獲取到就退出獲取,可以用來做做其他的事情,這樣更能調高執行緒的效率(這裡是以非公平鎖為例)。

public boolean tryLock() {
    return sync.nonfairTryAcquire(1);
}
複製程式碼

另外tryLock還有帶有時間限制的方法,表示在指定時間內進行獲取鎖的嘗試。

public boolean tryLock(long timeout, TimeUnit unit) throws InterruptedException {
    return sync.tryAcquireNanos(1, unit.toNanos(timeout));
}
public final boolean tryAcquireNanos(int arg, long nanosTimeout) throws InterruptedException {
    if (Thread.interrupted())
        throw new InterruptedException();
    return tryAcquire(arg) ||
        doAcquireNanos(arg, nanosTimeout);
}
複製程式碼

這裡帶時間嘗試的方法是可以中斷的,沒有中斷的情況下首先先去嘗試性的獲取一次鎖,如果不成功,則進行規定時間的輪詢政策.

private boolean doAcquireNanos(int arg, long nanosTimeout) throws InterruptedException {
   //超時直接返回  
   if (nanosTimeout <= 0L) return false;
   //當前時間+嘗試的時間
    final long deadline = System.nanoTime() + nanosTimeout;
   //加入等待佇列
    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;
            }
          //超時返回
            nanosTimeout = deadline - System.nanoTime();
            if (nanosTimeout <= 0L)
                return false;
           //失敗後的狀態處理,中斷會丟擲異常
            if (shouldParkAfterFailedAcquire(p, node) && nanosTimeout > spinForTimeoutThreshold)
                LockSupport.parkNanos(this, nanosTimeout);
            if (Thread.interrupted())
                throw new InterruptedException();
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}
複製程式碼

首先是嘗試去拿鎖,如果拿到了直接返回,如果沒拿到繼續判斷嘗試的時間是否大於1000L,如果大於這麼多就要把物件掛起,如果最終都沒有拿到,在finnally中將此節點從同步佇列中移除,以失敗告終.

另外還有一個帶中斷獲取鎖方式lockInterruptibly和帶時間的tryLock方法相似.

public void lockInterruptibly() throws InterruptedException {
    sync.acquireInterruptibly(1);
}
public final void acquireInterruptibly(int arg)
        throws InterruptedException {
    if (Thread.interrupted())
        throw new InterruptedException();
    if (!tryAcquire(arg))
        doAcquireInterruptibly(arg);
}
複製程式碼

doAcquireInterruptiblydoAcquireNanos方法也幾乎一樣,只是doAcquireInterruptibly方法沒有時間的限制,發生中斷都會丟擲異常.

condition方法

通過newCondition方法就可以獲得一個condition物件,此物件主要是用來點對點的去讓一個執行緒暫停和啟用,看一下具體的方法

public Condition newCondition() {
    return sync.newCondition();
}
 final ConditionObject newCondition() {
   	return new ConditionObject();
 }
複製程式碼

這裡先不介紹裡面的細節了,有興趣的可以自己看一下,也可以等我下一篇文章介紹。

小結

上面我們介紹了lock和unlock以及lock的擴充套件方法的實現,本文並沒有詳細介紹AbstractQueuedSynchronizer相關的內容,因為當初筆者在看的時候就是混在一起看的,還牽涉到condition相關的內容,導致整個人的都看的很暈,分不清楚什麼是什麼,因此在寫本文就將其內容全部分開了,上文中也給出了AbstractQueuedSynchronizer相關的內容的連結地址。我們先了解每一個部分的內容,最終在看ReentrantLock的內容,這樣更容易梳理和理解.

再次總結一下本文的內容,

1.首先根據建構函式選擇合適的鎖方式(公平鎖和非公平鎖),

2.然後我們使用lock方法嘗試獲取鎖,如果獲取到鎖返回

3.假如沒有獲取到鎖,就會被加入到一個等待的同步佇列中,等待獲取鎖

4.當其他持有鎖的方法呼叫unlock方法,則就會從佇列頭部拿到下一個等待的執行緒,讓他持有鎖.

5.迴圈直到所有等待的執行緒都獲得鎖.

另外本文還講述了可中斷方式獲取鎖和指定時間內嘗試獲取鎖,使用這些方法可以防止執行緒一直等待而不一定獲得鎖的情況,特別是非公平鎖存在插隊的情況。

結語

本文均出自個人的觀點,如果有什麼表述錯誤或者紕漏的地方,歡迎指正。

與君共勉!!!

相關文章