Java併發編程筆記之ReentrantLock源碼分析

ReentrantLock是可重入的獨佔鎖,同時只能有一個線程能夠獲取該鎖,其餘獲取該鎖的線程會被阻塞後放入該鎖的AQS阻塞隊列裏面。java

首先咱們先看一下ReentrantLock的類圖結構,以下圖所示:數組

從類圖能夠知道,ReentrantLock最終仍是使用AQS來實現,而且根據參數決定內部是公平鎖仍是非公平鎖,默認是非公平鎖。安全

首先咱們先看ReentrantLock源碼,看到其構造函數及其參數,這是決定內部是公平鎖仍是非公平鎖,以下源碼所示:函數

public ReentrantLock() {
        sync = new NonfairSync();
}
 public ReentrantLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
}

其中類Sync直接繼承自AQS,它的子類NonfairSync和FairSync分別實現了獲取鎖的公平和非公平策略。ui

 

在這裏AQS的狀態值state表明線程獲取該鎖的可重入次數,默認狀況下state的值爲0,標示當前鎖沒有被任何線程持有,當一個線程第一次獲取該鎖的時候會嘗試使用CAS設置state的值爲1,若是CAS成功則當前線程獲取了該鎖,而後記錄該鎖的持有者爲當前線程,spa

在該線程沒有釋放鎖,第二次獲取該鎖後,狀態值會加1,被設置爲2,這就是可重入次數,在該線程釋放該鎖的時候,會嘗試使用CAS讓狀態值減1,若是減1 後狀態值爲0 則當前線程釋放該鎖。線程

 

接下來咱們看一下ReentrantLock是如何獲取鎖的,以下:code

  1.void lock() 當一個線程調用該方法,說明該線程但願獲取該鎖,若是鎖當前沒有被其它線程佔用而且當前線程以前沒有獲取該鎖,則當前線程會獲取到該鎖,而後設置當前鎖的擁有者爲當前線程,並設置 AQS 的狀態值爲 1 後直接返回。blog

若是當前線程以前已經獲取過該鎖,則此次只是簡單的把 AQS 的狀態值 status 加 1 後返回。 若是該鎖已經被其它線程持有,則調用該方法的線程會被放入 AQS 隊列後阻塞掛起。源碼以下:繼承

  public void lock() {
        sync.lock();
    }

 

如上面代碼所示,ReentrantLock的lock()是委託給sync類,根據建立ReentrantLock的時候,構造函數選擇sync的實現是NonfairSync或者FairSync,這裏先看sync的子類NonfairSync的狀況,也就是非公平鎖的時候,源碼以下:

final void lock() {
  //(1)CAS設置狀態值
  if (compareAndSetState(0, 1))
      setExclusiveOwnerThread(Thread.currentThread());
  else
  //(2)調用AQS的acquire方法
      acquire(1);
}

如上面代碼所示,代碼(1)由於默認AQS的狀態值爲0,因此第一個調用Lock的線程會經過CAS設置狀態值爲1,CAS成功則表明當前線程獲取到了鎖,而後setExclusiveOwnerThread 設置了該鎖持有者是當前線程。

若是這時候有其餘線程調用lock方法企圖獲取該鎖,執行代碼(1)CAS會失敗,而後會調用AQS的acquire方法,這裏注意傳遞參數爲1,接下來咱們看AQS的acquire的核心代碼,以下:

 

   public final void acquire(int arg) {
        //(3)調用ReentrantLock重寫的tryAcquire方法
        if (!tryAcquire(arg) &&
            // tryAcquiref返回false會把當前線程放入AQS阻塞隊列
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }

 

以前說過 AQS 並無提供可用的 tryAcquire 方法,tryAcquire 方法須要子類本身定製化,因此這裏代碼(3)會調用 ReentrantLock 重寫的 tryAcquire 方法代碼。

這裏先看下非公平鎖的源碼代碼以下:

protected final boolean tryAcquire(int acquires) {
            return nonfairTryAcquire(acquires);
}

final boolean nonfairTryAcquire(int acquires) {
  final Thread current = Thread.currentThread();
  int c = getState();
  //(4)當前AQS狀態值爲0
  if (c == 0) {
      if (compareAndSetState(0, acquires)) {
          setExclusiveOwnerThread(current);
          return true;
      }
  }//(5)當前線程是該鎖持有者
  else if (current == getExclusiveOwnerThread()) {
      int nextc = c + acquires;
      if (nextc < 0) // overflow
          throw new Error("Maximum lock count exceeded");
      setState(nextc);
      return true;
  }//(6)
  return false;
}

正如上面代碼(4)會看當前鎖的狀態值是否爲0,爲0則說明當前該鎖空閒,那麼就嘗試CAS獲取該鎖(嘗試將 AQS 的狀態值從 0 設置爲 1),並設置當前鎖的持有者爲當前線程返回返回 true。

若是當前狀態值不爲0 則說明該鎖已經被某個縣城持有,因此代碼(5)看當前線程是不是該鎖的持有者,若是當前線程是該鎖持有者,狀態值增長1,而後返回true。

若是當前線程不是鎖的持有者則返回 false, 而後會被放入 AQS 阻塞隊列。

 

到目前爲止,介紹完了非公平鎖的實現代碼,回過頭看看非公平鎖在這裏是怎麼體現的,首先非公平是說:先嚐試獲取鎖的線程並不必定比後嘗試獲取鎖的線程優先獲取鎖。

這裏假設線程 A 調用 lock()方法時候執行到了 nonfairTryAcquire 的代碼(4)發現當前狀態值不爲 0,因此執行代碼(5)發現當前線程不是線程持有者,則執行代碼(6)返回 false,而後當前線程會被放入了 AQS 阻塞隊列。

這時候線程 B 也調用了 lock() 方法執行到 nonfairTryAcquire 的代碼(4)時候發現當前狀態值爲 0 了(假設佔有該鎖的其它線程釋放了該鎖)因此經過 CAS 設置獲取到了該鎖。而明明是線程 A 先請求獲取的該鎖那,這就是非公平鎖的實現,

這裏線程 B 在獲取鎖前並無看當前 AQS 隊列裏面是否有比本身請求該鎖更早的線程,而是使用了搶奪策略。

 

好了,知道非公平鎖的實現了,那麼咱們接下來看一下公平鎖是如何實現的呢?

公平鎖的實現只須要看FairSync重寫的tryAcquire方法,源碼以下:

  protected final boolean tryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            int c = getState();
            //(7)當前AQS狀態值爲0
            if (c == 0) {
             //(8)公平性策略
                if (!hasQueuedPredecessors() &&
                    compareAndSetState(0, acquires)) {
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
            //(9)當前線程是該鎖持有者
            else if (current == getExclusiveOwnerThread()) {
                int nextc = c + acquires;
                if (nextc < 0)
                    throw new Error("Maximum lock count exceeded");

                setState(nextc);
                return true;
            }//(10)
            return false;
        }
    }

 

如上代碼公平性的tryAcquire策略與非公平鎖的相似,不一樣在於代碼(8)處設置CAS前添加了hasQueuedPredecessors 方法,該方法是實現公平性的核心代碼,源代碼以下:

  public final boolean hasQueuedPredecessors() {

        Node t = tail; // Read fields in reverse initialization order
        Node h = head;
        Node s;
        return h != t &&((s = h.next) == null || s.thread != Thread.currentThread());
    }

如上代碼所示,若是當前線程節點有前驅節點則返回true,不然若是當前AQS隊列爲空或者當前線程節點是AQS的第一個節點則返回false。

其中若是h == t 則說明當前隊列爲空則直接返回false,若是h != t 而且 (s = h.next) ==null 說明有一個元素將要做爲AQS的第一個節點入隊列,那麼返回true, 若是h != t 而且 (s = h.next) !=null 而且 s.thread != Thread.currentThread() 則說明隊列裏面的第一個元素不是當前線程則返回 true。

 

  2.void lockInterruptibly() 與 lock() 方法相似,不一樣在於該方法對中斷響應,就是當前線程在調用該方式時候,若是其它線程調用了當前線程線程的 interrupt()方法,當前線程會拋出 InterruptedException 異常而後返回,源代碼以下:

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))
       //調用AQS可被狀態的方法
       doAcquireInterruptibly(arg);
}

 

  3.boolean tryLock() 嘗試獲取鎖,若是當前該鎖沒有被其它線程持有則當前線程獲取該鎖並返回 true, 否者返回 false,注意該方法不會引發當前線程阻塞。源碼以下所示:

public boolean tryLock() {
   return sync.nonfairTryAcquire(1);
}

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() 方法相似,因此 tryLock() 使用的是非公平策略。

 

  4.boolean tryLock(long timeout, TimeUnit unit) 嘗試獲取鎖與 tryLock()不一樣在於設置了超時時間,若是超時沒有獲取該鎖則返回 false。源代碼以下:

 public boolean tryLock(long timeout, TimeUnit unit)throws InterruptedException {
        //調用AQS的tryAcquireNanos方法。
        return sync.tryAcquireNanos(1, unit.toNanos(timeout));
    }

 

 

接下來咱們要看一下,ReentrantLock是如何釋放鎖的。

  1.void unlock() 嘗試釋放鎖,若是當前線程持有該鎖,調用該方法會讓該線程對該線程持有的 AQS 狀態值減一,若是減去 1 後當前狀態值爲 0 則當前線程會釋放對該鎖的持有,否者僅僅減一而已。

若是當前線程沒有持有該鎖調用了該方法則會拋出 IllegalMonitorStateException 異常 ,源代碼以下:

  public void unlock() {
        sync.release(1);
    }

   protected final boolean tryRelease(int releases) {
      //(11)若是不是鎖持有者調用UNlock則拋出異常。
       int c = getState() - releases;
       if (Thread.currentThread() != getExclusiveOwnerThread())
           throw new IllegalMonitorStateException();
       boolean free = false;
      //(12)若是當前可重入次數爲0,則清空鎖持有線程
       if (c == 0) {
           free = true;
           setExclusiveOwnerThread(null);
       }
       //(13)設置可重入次數爲原始值-1
       setState(c);
       return free;
   }

如上代碼所示(11)若是當前線程不是該鎖持有者則直接拋異常,不然,看狀態值剩餘值是否爲0,爲0 則說明當前線程要釋放對該鎖的持有權,則執行代碼(12)把當前鎖持有者設置爲null。

若是剩餘值不爲0,則僅僅讓當前線程對該鎖的可重入次數減1。

 

到目前基本瞭解了ReentrantLock的原理,那麼接下來咱們是否能夠用ReentrantLock來實現一個簡單的線程安全的list呢?

例子以下:

import java.util.ArrayList;
import java.util.concurrent.locks.ReentrantLock;

/**
 * Created by cong on 2018/6/12.
 */
public class ReentrantLockList {

    //線程不安全的list
    private ArrayList<String> array = new ArrayList<String>();
    //獨佔鎖
    private volatile ReentrantLock lock = new ReentrantLock();

    //添加元素
    public void add(String e) {

        lock.lock();
        try {
            array.add(e);
        } finally {
            lock.unlock();
        }
    }
    //刪元素
    public void remove(String e) {

        lock.lock();
        try {
            array.remove(e);
        } finally {
            lock.unlock();

        }
    }

    //獲取數據
    public String get(int index) {

        lock.lock();
        try {
            return array.get(index);
        } finally {
            lock.unlock();

        }
    }
    
}

 

如上代碼經過在操做 array 元素前進行加鎖保證同時只有一個線程能夠對 array 數組進行修改,可是同時也只能有一個線程對 array 元素進行訪問。

 

最後幾個圖加深前面所學的內容,以下圖所示:

如上圖,假如線程 Thread1,Thread2,Thread3 同時嘗試獲取獨佔鎖 ReentrantLock,假設 Thread1 獲取到了,則 Thread2 和 Thread3 就會被轉換爲 Node 節點後放入 ReentrantLock 對應的 AQS 阻塞隊列後阻塞掛起。

 

如上圖,假設 Thread1 獲取鎖後調用了對應的鎖建立的條件變量 1,那麼 Thread1 就會釋放獲取到的鎖,而後當前線程就會被轉換爲 Node 節點後插入到條件變量 1 的條件隊列,因爲 Thread1 釋放了鎖,

因此阻塞到 AQS 隊列裏面 Thread2 和 Thread3 就有機會獲取到該鎖,假如使用的公平策略,那麼這時候 Thread2 會獲取到該鎖,會從 AQS 隊列裏面移除 Thread2 對應的 Node 節點。

相關文章
相關標籤/搜索