【JDK】JDK源碼分析-ReentrantLock

  • 2019 年 10 月 3 日
  • 筆記

概述

 

在 JDK 1.5 以前,鎖的實現只能用 synchronized 關鍵字;1.5 開始提供了 ReentrantLock,它是 API 層面的鎖。先看下 ReentrantLock 的類簽名以及如何使用:

public class ReentrantLock implements Lock, java.io.Serializable {}

典型用法:

public void m() {    lock.lock();  // block until condition holds    try {      // ... method body    } finally {      lock.unlock()    }  }

該用法和使用 synchronized 關鍵字效果是一樣的。既然有了 synchronized,為什麼又會有 Lock 呢?相比於 synchronized,其實 ReentrantLock 的出現並不重複,它增加了不少功能,下面先簡單介紹幾個概念。

 

公平鎖&非公平鎖:所謂鎖是否公平,簡單理解就是一系列執行緒獲取到鎖的順序是否遵循「先來後到」。即,如果先申請鎖的執行緒先獲取到鎖,就是公平鎖;否則就是非公平鎖。ReentrantLock 的默認實現和 synchronized 都是非公平鎖。

 

可重入鎖:鎖是否可重入,就是一個執行緒是否可以多次獲取同一個鎖,若是,就是可重入鎖。ReentrantLock 和 synchronized 都是可重入鎖。

 

程式碼分析

 

構造器

 

ReentrantLock 有兩個構造器,分別如下:

private final Sync sync;    // 構造一個 ReentrantLock 實例(非公平鎖)  public ReentrantLock() {      sync = new NonfairSync();  }    // 構造一個 ReentrantLock 實例(指定是否公平)  public ReentrantLock(boolean fair) {      sync = fair ? new FairSync() : new NonfairSync();  }

可以看到,兩個構造器都是初始化一個 Sync 類型的成員變數。而且,當 boolean 值 fair 為 true 時,初始化的 sync 為 FairSync,為 false 時初始化為 NonFairSync,二者分別表示「公平鎖」和「非公平鎖」。可以看到無參構造默認是非公平鎖。

 

常用方法

 

ReentrantLock 常用的方法就是 Lock 介面定義的幾個方法,如下:

// 獲取鎖(阻塞式)  public void lock() {      sync.lock();  }    // 獲取鎖(響應中斷)  public void lockInterruptibly() throws InterruptedException {      sync.acquireInterruptibly(1);  }    // 嘗試獲取鎖  public boolean tryLock() {      return sync.nonfairTryAcquire(1);  }    // 嘗試獲取鎖(有超時等待)  public boolean tryLock(long timeout, TimeUnit unit)          throws InterruptedException {      return sync.tryAcquireNanos(1, unit.toNanos(timeout));  }    // 釋放鎖  public void unlock() {      sync.release(1);  }

可以看到,這幾個方法內部都是通過調用 Sync 類(或其子類)的方法來實現,因此先從 Sync 類入手分析,程式碼如下(部分省略):

// 抽象類,繼承了 AQS  abstract static class Sync extends AbstractQueuedSynchronizer {        // 獲取鎖的方法,由子類實現      abstract void lock();        // 非公平鎖的 tryLock 方法實現      final boolean nonfairTryAcquire(int acquires) {          final Thread current = Thread.currentThread();          // 獲取 AQS 的 state 變數          int c = getState();          // 若為 0,表示當前沒有被其他執行緒佔用          if (c == 0) {              // CAS 修改 state,若修改成功,表示成功獲取資源              if (compareAndSetState(0, acquires)) {                  // 將當前執行緒設置為 owner,到這裡表示當前執行緒成功獲取資源                  setExclusiveOwnerThread(current);                  return true;              }          }          // state 不為 0,且 owner 為當前執行緒          // 表示當前執行緒已經獲取到了資源,這裡表示“重入”          else if (current == getExclusiveOwnerThread()) {              int nextc = c + acquires;              if (nextc < 0) // overflow                  throw new Error("Maximum lock count exceeded");              // 修改 state 值(因為當前執行緒已經獲取資源,不存在競爭,因此無需 CAS 操作)              setState(nextc);              return true;          }          return false;      }        // 釋放鎖操作(對 state 做減法)      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;              // 成功釋放後將 owner 設為空              setExclusiveOwnerThread(null);          }          // 修改 state 的值          // PS: 因為可能存在“重入”,因此一次釋放操作後當前執行緒仍有可能佔用資源,          // 所以不會直接把 state 設為 0          setState(c);          return free;      }        // 其他方法...        final boolean isLocked() {          return getState() != 0;      }  }

Sync 類繼承自 AQS,其中 nonfairTryAcquire 方法是非公平鎖 tryAcquire 方法的實現。

 

從上面程式碼可以看出,鎖的獲取和釋放是通過修改 AQS 的 state 變數來實現的。lock 方法可以看做對 state 執行“加法”操作,而 unlock 可以看做對 state 執行“減法”操作,當 state 為 0 時,表示當前沒有執行緒佔用資源。

 

公平鎖&非公平鎖

 

(1)非公平鎖 NonFairSync:

static final class NonfairSync extends Sync {        final void lock() {          // CAS 嘗試將 state 值修改為 1          if (compareAndSetState(0, 1))              // 若修改成功,則將當前執行緒設為 owner,表示成功獲取鎖              setExclusiveOwnerThread(Thread.currentThread());          // 若獲取失敗,則執行 AQS 的 acquire 方法(獨佔模式獲取資源)          else              acquire(1);      }        protected final boolean tryAcquire(int acquires) {          return nonfairTryAcquire(acquires);      }  }

可以看到,非公平鎖的 lock 操作為:先嘗試以 CAS 方式修改 state 的值,若修改成功,則表示成功獲取到鎖,將 owner 設為當前執行緒;否則就執行 AQS 中的 acquire 方法,具體可參考前文「JDK源碼分析-AbstractQueuedSynchronizer(2)」,這裡不再贅述。

 

(2)公平鎖 FairSync:

static final class FairSync extends Sync {        final void lock() {          acquire(1);      }        // 公平鎖的 tryAcquire 實現      protected final boolean tryAcquire(int acquires) {          final Thread current = Thread.currentThread();          int c = getState();          // state 為 0,表示資源未被佔用          if (c == 0) {              // 若隊列中有其他執行緒在排隊等待,則返回 false,表示獲取失敗;              //   否則,再嘗試去修改 state 的值              // PS: 這裡是公平鎖與非公平鎖的區別所在              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,即首先判斷主隊列中是否有其他執行緒在等待,當沒有其他執行緒在排隊時再去獲取,否則獲取失敗。

 

hasQueuedPredecessors 在 AQS 中實現如下:

/**   * Queries whether any threads have been waiting to acquire longer   * than the current thread.   */  public final boolean hasQueuedPredecessors() {      // The correctness of this depends on head being initialized      // before tail and on head.next being accurate if the current      // thread is first in queue.      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());  }

 

小結

 

synchronized 與 ReentrantLock 比較:

相同點:二者都是互斥鎖,可重入,默認都是非公平鎖。

不同點:synchronized 是語法層面實現,自動獲取鎖和釋放鎖;ReentrantLock 是 API 層面實現,手動獲取鎖和釋放鎖。

 

ReentrantLock 相比 synchronized 的優勢:

1. 可響應中斷;

2. 獲取鎖可設置超時;

3. 可實現公平鎖;

4. 可綁定多個條件(Condition)。

 

JDK 1.6 以後,synchronized 與 ReentrantLock 性能基本持平,JVM 未來的性能優化也會更偏向於原生的 synchronized。因此,如何選擇還要根據實際需求,性能不再是不選擇 synchronized 的原因了。

 

相關閱讀:

JDK源碼分析-Lock&Condition

JDK源碼分析-AbstractQueuedSynchronizer(2)

 

 

Stay hungry, stay foolish.

PS: 本文首發於微信公眾號【WriteOnRead】。