1. 程式人生 > >ReentrantReadWriteLock 讀寫鎖

ReentrantReadWriteLock 讀寫鎖

ReentrantReadWriteLock 讀寫鎖

讀寫鎖不同於 ReentrantLock ,ReentrantLock 是排他鎖同一時刻只允許一個執行緒訪問,而讀寫鎖同一時刻允許多個讀執行緒訪問,但是在寫執行緒操作時,所有的讀寫操作均被阻塞。

讀寫狀態實現

如何通過一個 int 值記錄讀狀態,寫狀態呢 ?

在 ReentrantReadWriteLock 中通過對同步狀態值進行“按位切割”,因為 int 佔 32 位 bit,故一分為二,採用高 16 位表示讀狀態,低 16 位表示寫狀態。

如何獲取寫狀態值

我們假設同步狀態值轉換為二進位制如下:

0000 0000 0000 0010
| 0000 0000 0000 0101 複製程式碼

上述同步狀態值表示:讀狀態為 2, 寫狀態為 5

那麼我們如何獲取狀態值呢 ? 我們思考下位的相關運算 :

  • 位與操作(&) 兩個數同為 1 則為 1, 否則為 0
  • 位或操作(|) 兩個數有一個為 1 則為 1, 否則為 0

瞭解了 & ,| 運算規律我們是不是可以這樣操作呢,將同步狀態值與如下二進位制進行 & 運算

0000 0000 0000 0000 | 1111 1111 1111 1111
複製程式碼

結果可得

0000 0000 0000 0000 | 0000 0000 0000 0101
複製程式碼

也就是寫狀態的二進位制表示,值為 5. 那麼

0000 0000 0000 0000 | 1111 1111 1111
1111 複製程式碼

該位與運算元轉成十進位制也即是 65535 (2^15 + 2^14 + ..... + 2^0),由等比數列可知等於 2^16 - 1, 也等於 (1 << 16) - 1 。 這也是 ReentrantReadWriteLock 內部定義的常量實現 :

static final int SHARED_SHIFT   = 16;
static final int SHARED_UNIT    = (1 << SHARED_SHIFT);
static final int MAX_COUNT      = (1 << SHARED_SHIFT) - 1
; static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1; 複製程式碼

如何獲取讀狀態值

獲取讀狀態相比寫狀態來說就比較簡單了,只需同步狀態值 >> 右移 16 位即可

寫狀態值加一

當寫鎖重入的時候,如何更新寫狀態值呢 ? 我們知道狀態值的低 16 位表示寫狀態,那麼每次寫狀態加一操作相當於下面二進位制相加操作(逢二進一)

0000 0000 0000 0000 | 0000 0000 0000 0011
複製程式碼
0000 0000 0000 0000 | 0000 0000 0000 0001
複製程式碼

相加可得

0000 0000 0000 0000 | 0000 0000 0000 0100
複製程式碼

也就是寫狀態值由 3 加一變成 4;那麼對於寫狀態增加一時,也就是同步狀態值 S + 1 即可。

讀狀態值加一

讀狀態增加一,與寫狀態一樣;只不過因為是高 16 位表示讀狀態,故是同步狀態 S + (1 << 16).

構造

public ReentrantReadWriteLock() {
    this(false);
}

public ReentrantReadWriteLock(boolean fair) {
    sync = fair ? new FairSync() : new NonfairSync();
    readerLock = new ReadLock(this);
    writerLock = new WriteLock(this);
}
複製程式碼

writerLock() - 寫鎖

public ReentrantReadWriteLock.WriteLock writeLock() {
  return writerLock;
}
複製程式碼
獲取鎖

當執行 writeLock.lock() 的時候,實際上呼叫的是 sync.acquire(), 如下:

public void lock() {
    // 寫鎖是獨佔模式
    sync.acquire(1);
}
複製程式碼

下面我們看下 sync 的 tryAcquire 實現如下:

protected final boolean tryAcquire(int acquires) {
      /*
       * Walkthrough:
       * 1. If read count nonzero or write count nonzero
       *    and owner is a different thread, fail.
       * 2. If count would saturate, fail. (This can only
       *    happen if count is already nonzero.)
       * 3. Otherwise, this thread is eligible for lock if
       *    it is either a reentrant acquire or
       *    queue policy allows it. If so, update state
       *    and set owner.
       */
      Thread current = Thread.currentThread();
      // 獲取同步狀態值
      int c = getState();
      // 獲取寫狀態值
      int w = exclusiveCount(c);
      if (c != 0) {
          // (Note: if c != 0 and w == 0 then shared count != 0)
          // c != 0 說明此時已經有讀或有寫或有讀寫
          // 若 w == 0 說明此時有讀操作,則獲取寫鎖失敗
          // 若 w != 0 說明此時已經有寫操作
          // 若 current != getExclusiveOwnerThread() 說明當前獲取寫鎖的執行緒並非是寫鎖物件的持有者, 則重入失敗
          if (w == 0 || current != getExclusiveOwnerThread())
              return false;
          // 重入次數超過最大值 丟擲異常
          if (w + exclusiveCount(acquires) > MAX_COUNT)
              throw new Error("Maximum lock count exceeded");
          // Reentrant acquire
          // 設定狀態值
          setState(c + acquires);
          return true;
      }
      if (writerShouldBlock() ||
          !compareAndSetState(c, c + acquires))
          return false;
      // 設定當前執行緒為寫鎖的持有者
      setExclusiveOwnerThread(current);
      return true;
  }
複製程式碼

從程式碼的實現及註釋中所描述的內容,可得知以下場景會獲取寫鎖失敗 :

  • 當前已經有讀操作,則獲取寫鎖失敗
  • 當前已經有寫操作,但當前執行緒並非寫鎖物件的持有者,則獲取寫鎖失敗(也是重入失敗)
  • 當前沒有任何操作,CAS 更新狀態值失敗,則獲取寫鎖失敗
釋放鎖
protected final boolean tryRelease(int releases) {
    // 判斷當前執行緒是否為寫鎖物件的持有者
    if (!isHeldExclusively())
        throw new IllegalMonitorStateException();
    // 同步狀態值釋放
    int nextc = getState() - releases;
    // 判斷寫狀態是否為 0; 寫狀態為 0 說明寫鎖完成釋放
    boolean free = exclusiveCount(nextc) == 0;
    if (free)
        // 清空寫鎖的持有者
        setExclusiveOwnerThread(null);
    setState(nextc);
    return free;
}
複製程式碼

readLock() - 讀鎖

public ReentrantReadWriteLock.ReadLock  readLock()  {
  return readerLock;
}
複製程式碼
獲取鎖
public void lock() {
    sync.acquireShared(1);
}
複製程式碼

因為讀寫鎖是支援同時多個執行緒獲取讀鎖,所以呼叫的是 sync 共享式獲取同步狀態。 這裡針對讀鎖的獲取和釋放我們簡化下實現忽略對讀鎖計數統計的操作。

protected final int tryAcquireShared(int unused) {
    Thread current = Thread.currentThread();
   
    for (;;) {
        // 獲取讀狀態值
        int c = getState();
        // exclusiveCount(c) != 0 說明有寫操作
        // getExclusiveOwnerThread != current 說明當前執行緒非寫鎖的物件持有者; 則獲取讀鎖失敗
        // 若 getExclusiveOwnerThread == current 也就是說明執行緒獲取寫鎖之後是可以繼續獲取讀鎖的
        if (exclusiveCount(c) != 0) {
            if (getExclusiveOwnerThread() != current)
                return -1;
            // else we hold the exclusive lock; blocking here
            // would cause deadlock.
        }
        if (sharedCount(c) == MAX_COUNT)
            throw new Error("Maximum lock count exceeded");
        if (compareAndSetState(c, c + SHARED_UNIT)) {
            // 忽略讀鎖計數統計的操作 
            return 1;
        }
    }
}
複製程式碼
釋放鎖
protected final boolean tryReleaseShared(int unused) {
    Thread current = Thread.currentThread();
    // 忽略讀鎖計數統計的操作 
    for (;;) {
        int c = getState();
        int nextc = c - SHARED_UNIT;
        if (compareAndSetState(c, nextc))
            // Releasing the read lock has no effect on readers,
            // but it may allow waiting writers to proceed if
            // both read and write locks are now free.
            return nextc == 0;
    }
}
複製程式碼

小結

  • 讀寫鎖的實現關鍵在於如何通過一個 int 值,分別記錄讀寫狀態。(採用按位切割,高 16 位為讀狀態,低 16 位狀態)
  • 何時可以獲取讀鎖 ? 獲取寫鎖的執行緒可以再次獲取讀鎖,獲取讀鎖的執行緒數未超過 2^16 - 1 時是可以獲取讀鎖。
  • 何時可以獲取寫鎖 ? 已經有讀鎖在操作則不可用獲取寫鎖