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;
复制代码

扫描二维码关注公众号,回复: 4361111 查看本文章

如何获取读状态值

获取读状态相比写状态来说就比较简单了,只需同步状态值 >> 右移 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 时是可以获取读锁。
  • 何时可以获取写锁 ? 已经有读锁在操作则不可用获取写锁

猜你喜欢

转载自juejin.im/post/5c06282451882530544f145d