Java中讀寫鎖的設計以及實現,不懂進來 讀寫鎖遵守以下三條基本原則 讀寫鎖如何實現 獲取讀鎖 釋放讀鎖 獲取寫鎖 釋放寫鎖 鎖的降級 RRW需要注意的問題

針對讀多寫少的場景,Java提供了另外一個實現Lock接口的讀寫鎖ReentrantReadWriteLock(RRW),之前分析過ReentrantLock是一個獨佔鎖,同一時間只允許一個線程訪問。

而 RRW 允許多個讀線程同時訪問,但不允許寫線程和讀線程、寫線程和寫線程同時訪問。

讀寫鎖內部維護了兩個鎖,一個是用於讀操作的ReadLock,一個是用於寫操作的 WriteLock。

讀寫鎖遵守以下三條基本原則

允許多個線程同時讀共享變量;
只允許一個線程寫共享變量;
如果一個寫線程正在執行寫操作,此時禁止讀線程讀共享變量。

讀寫鎖如何實現

RRW也是基於AQS實現的,它的自定義同步器(繼承自AQS)需要在同步狀態state上維護多個讀線程和一個寫線程的狀態。RRW的做法是使用高低位來實現一個整形控制兩種狀態,一個int佔4個字節,一個字節8位。所以高16位表示讀,低16位表示寫。

abstract static class Sync extends AbstractQueuedSynchronizer {

  static final int SHARED_SHIFT   = 16;

  // 10000000000000000(65536)
  static final int SHARED_UNIT    = (1 << SHARED_SHIFT);

  // 65535
  static final int MAX_COUNT      = (1 << SHARED_SHIFT) - 1;

  //1111111111111111
  static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1;

  // 讀鎖(共享鎖)的數量,只計算高16位的值
  static int sharedCount(int c)    { return c >>> SHARED_SHIFT; }

  // 寫鎖(獨佔鎖)的數量
  static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; }
 }

獲取讀鎖

當線程獲取讀鎖時,首先判斷同步狀態低16位,如果存在寫鎖,則獲取鎖失敗,進入CLH隊列阻塞,反之,判斷當前線程是否應該被阻塞,如果不應該阻塞則嘗試 CAS 同步狀態,獲取成功更新同步鎖爲讀狀態。

protected final int tryAcquireShared(int unused) {
           
  Thread current = Thread.currentThread();
  int c = getState();
  // 如果當前已經有寫鎖了,則獲取失敗
  if (exclusiveCount(c) != 0 &&
      getExclusiveOwnerThread() != current)
      return -1;
  // 獲取讀鎖數量
  int r = sharedCount(c);

  // 非公平鎖實現中readerShouldBlock()返回true表示CLH隊列中有正在排隊的寫鎖
  // CAS設置讀鎖的狀態值
  if (!readerShouldBlock() &&
      r < MAX_COUNT &&
      compareAndSetState(c, c + SHARED_UNIT)) {

      // 省略記錄獲取readLock次數的代碼

      return 1;
  }

  // 針對上面失敗的條件進行再次處理
  return fullTryAcquireShared(current);
}

final int fullTryAcquireShared(Thread current) {
  
  // 無線循環
  for (;;) {
    int c = getState();
    if (exclusiveCount(c) != 0) {
      // 如果不是當前線程持有寫鎖,則進入CLH隊列阻塞
      if (getExclusiveOwnerThread() != current)
        return -1;
    } 

    // 如果reader應該被阻塞
    else if (readerShouldBlock()) {
        // Make sure we're not acquiring read lock reentrantly
        if (firstReader == current) {
            // assert firstReaderHoldCount > 0;
        } else {
            if (rh == null) {
                rh = cachedHoldCounter;
                if (rh == null || rh.tid != getThreadId(current)) {
                    rh = readHolds.get();
                    if (rh.count == 0)
                        readHolds.remove();
                }
            }
            // 當前線程沒有持有讀鎖,即不存在鎖重入情況。則進入CLH隊列阻塞
            if (rh.count == 0)
                return -1;
        }
    }

    // 共享鎖的如果超出了限制
    if (sharedCount(c) == MAX_COUNT)
        throw new Error("Maximum lock count exceeded");

    // CAS設置狀態值
    if (compareAndSetState(c, c + SHARED_UNIT)) {
      
      // 省略記錄readLock次數的代碼

      return 1;
    }
  }

}

SHARED_UNIT 的值是65536,也就是說,當第一次獲取讀鎖的後,state的值就變成了65536。

在公平鎖的實現中當CLH隊列中有排隊的線程, readerShouldBlock() 方法就會返回爲true。非公平鎖的實現中則是當CLH隊列中存在等待獲取寫鎖的線程就返回true

還需要注意的是獲取讀鎖的時候,如果當前線程已經持有寫鎖,是仍然能獲取讀鎖成功的。後面會提到鎖的降級,如果你對那裏的代碼有疑問,可以在回過頭來看看這裏申請鎖的代碼

釋放讀鎖

protected final boolean tryReleaseShared(int unused) {
           
  for (;;) {
    int c = getState();
    // 減去65536
    int nextc = c - SHARED_UNIT;
    // 只有當state的值變成0纔會真正的釋放鎖
    if (compareAndSetState(c, nextc))
        return nextc == 0;
}
}

釋放鎖時,state的值需要減去65536,因爲當第一次獲取讀鎖後,state值變成了65536。

任何一個線程釋放讀鎖的時候只有在 state==0 的時候才真正釋放了鎖,比如有100個線程獲取了讀鎖,只有最後一個線程執行 tryReleaseShared 方法時才真正釋放了鎖,此時會喚醒CLH隊列中的排隊線程。

獲取寫鎖

一個線程嘗試獲取寫鎖時,會先判斷同步狀態 state 是否爲0。如果 state 等於 0,說明暫時沒有其它線程獲取鎖;如果 state 不等於 0,則說明有其它線程獲取了鎖。

此時再判斷state的低16位(w)是否爲0,如果w爲0,表示其他線程獲取了讀鎖,此時進入CLH隊列進行阻塞等待。

如果w不爲0,則說明其他線程獲取了寫鎖,此時需要判斷獲取了寫鎖的是不是當前線程,如果不是則進入CLH隊列進行阻塞等待,如果獲取了寫鎖的是當前線程,則判斷當前線程獲取寫鎖是否超過了最大次數,若超過,拋出異常。反之則更新同步狀態。

// 獲取寫鎖
protected final boolean tryAcquire(int acquires) {
           
  Thread current = Thread.currentThread();
  int c = getState();
  int w = exclusiveCount(c);

  // 判斷state是否爲0
  if (c != 0) {
      // 獲取鎖失敗
      if (w == 0 || current != getExclusiveOwnerThread())
          return false;

      // 判斷當前線程獲取寫鎖是否超出了最大次數65535
      if (w + exclusiveCount(acquires) > MAX_COUNT)
          throw new Error("Maximum lock count exceeded");
      
      // 鎖重入
      setState(c + acquires);
      return true;
  }
  // 非公平鎖實現中writerShouldBlock()永遠返回爲false
  // CAS修改state的值
  if (writerShouldBlock() ||
      !compareAndSetState(c, c + acquires))
      return false;

  // CAS成功後,設置當前線程爲擁有獨佔鎖的線程
  setExclusiveOwnerThread(current);
  return true;
}

在公平鎖的實現中當CLH隊列中存在排隊的線程,那麼 writerShouldBlock() 方法就會返回爲true,此時獲取寫鎖的線程就會被阻塞。

釋放寫鎖

釋放寫鎖的邏輯比較簡單

protected final boolean tryRelease(int releases) {
  // 寫鎖是否被當前線程持有
  if (!isHeldExclusively())
      throw new IllegalMonitorStateException();
  
  int nextc = getState() - releases;
  boolean free = exclusiveCount(nextc) == 0;

  // 沒有其他線程持有寫鎖
  if (free)
      setExclusiveOwnerThread(null);
  setState(nextc);
  return free;
}
鎖的升級?
// 準備讀緩存
readLock.lock();
try {
  v = map.get(key);
  if(v == null) {
    writeLock.lock();
    try {
      if(map.get(key) != null) {
        return map.get(key);
      }

      // 更新緩存代碼,省略
    } finally {
      writeLock.unlock();
    }
  }
} finally {
  readLock.unlock();
}

對於上面獲取緩存數據(這也是RRW的應用場景)的代碼,先是獲取讀鎖,然後再升級爲寫鎖,這樣的行爲叫做鎖的升級。可惜RRW不支持,這樣會導致寫鎖永久等待,最終導致線程被永久阻塞。所以 鎖的升級是不允許的 。

鎖的降級

雖然鎖的升級不允許,但是鎖的降級卻是可以的。

ReentrantReadWriteLock lock = new ReentrantReadWriteLock();

ReadLock readLock = lock.readLock();

WriteLock writeLock = lock.writeLock();

Map<String, String> dataMap = new HashMap();

public void processCacheData() {
  readLock.lock();

  if(!cacheValid()) {

    // 釋放讀鎖,因爲不允許
    readLock.unlock();

    writeLock.lock();

    try {
      if(!cacheValid()) {
          dataMap.put("key", "think123");
      }

      // 降級爲讀鎖
      readLock.lock();
    } finally {
        writeLock.unlock();
    }
  }

  try {
    // 仍然持有讀鎖
    System.out.println(dataMap);
  } finally {
      readLock.unlock();
  }
}

public boolean cacheValid() {
    return !dataMap.isEmpty();
}

RRW需要注意的問題

在讀取很多、寫入很少的情況下,RRW 會使寫入線程遭遇飢餓(Starvation)問題,也就是說寫入線程會因遲遲無法競爭到鎖而一直處於等待狀態。
寫鎖支持條件變量,讀鎖不支持。讀鎖調用newCondition() 會拋出UnsupportedOperationException 異常

來源:https://www.tuicool.com/articles/NFn2IrN

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章