Javaでの読み取り/書き込みロックの設計と実装、入り方がわからない

読み取りが多く書き込みが少ないシナリオでは、Javaは、ロックインターフェイスを実装する別の読み取り/書き込みロックReentrantReadWriteLock(RRW)を提供します。ReentrantLockは排他ロックであり、同時にアクセスできるのは1つのスレッドのみであることが分析されています。

RRWは、複数のリーダースレッドによる同時アクセスを許可しますが、ライタースレッドとリーダースレッド、およびライタースレッドとライタースレッドによる同時アクセスは許可しません。

読み取り/書き込みロックは、内部で2つのロックを維持します。1つは読み取り操作用のReadLockで、もう1つは書き込み操作用のWriteLockです。

読み取り/書き込みロックは、次の3つの基本原則に準拠しています。

複数のスレッド
同時に共有変数読み取ることができます。1つのスレッドのみが共有変数を書き込むことができます。
書き込みスレッドが書き込み操作を実行している場合、リーダースレッドは現時点で共有変数を読み取ることが禁止されています。

読み取り/書き込みロックを実装する方法

RRWもAQSに基づいて実装され、そのカスタムシンクロナイザー(AQSから継承)は、同期状態の状態で複数のリーダースレッドと1つのライタースレッドの状態を維持する必要があります。RRWの方法は、上位ビットと下位ビットを使用して、2つの状態のシェーピング制御を実現することです。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です。これは、読み取りロックが初めて取得されたときに、状態の値が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;
}
}

ロックを解除するときは、65536から状態の値を引く必要があります。これは、読み取りロックを初めて取得したときに、状態の値が65536になるためです。

いずれかのスレッドが読み取りロックを解放すると、state == 0の場合にのみロックが実際に解放されます。たとえば、100スレッドが読み取りロックを取得すると、最後のスレッドのみがtryReleaseSharedメソッドを実行して実際にロックを解放し、ウェイクアップします。この時点で、CLHキュー内のキューに入れられたスレッド。

書き込みロックを取得する

スレッドが書き込みロックを取得しようとすると、最初に同期状態の状態が0であるかどうかが判別されます。状態が0の場合は、当面の間、他のスレッドがロックを取得していないことを意味します。状態が0の場合は、他のスレッドがロックを取得したことを意味します。

このとき、状態の下位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をスローします

おすすめ

転載: blog.csdn.net/doubututou/article/details/111317098