当前位置: 代码迷 >> 综合 >> Java 并发 (8) -- ReentrantReadWriteLock 类
  详细解决方案

Java 并发 (8) -- ReentrantReadWriteLock 类

热度:17   发布时间:2023-12-16 13:15:47.0

文章目录

  • 1. 简介
  • 2. 精讲
    • 1. 读写锁的概念
      • 1. ReentrantReadWriteLock 的特性
    • 2. 读写锁的具体实现
      • 1. 读锁状态的设计
      • 2. 锁的获取
        • 1. 写锁的获取
        • 2. 读锁的获取
        • 3. 总结
      • 3. 锁的释放
        • 1. 写锁的释放
        • 2. 读锁的释放
        • 3. 总结
    • 3. ReentrantReadWriteLock 中的其他方法
      • 1. getOwner()
      • 2. getReadLockCount()
      • 3. getReadHoldCount()
      • 4. getWriteHoldCount()
    • 4. 读写锁总结

1. 简介

ReentrantReadWriteLock 允许多个读线程同时访问,但是不允许写线程和读线程 、写线程和写线程同时访问。读写锁内部维护了两个锁:一个是用于读操作的 ReadLock,一个是用于写操作的 WriteLock。读写锁可以保证多个线程可以同时读,所以在读操作远大于写操作的时候,读写锁非常有用

读写锁同样依赖自定义同步器来实现同步功能,而读写状态就是其同步器的同步状态。ReentrantLock 中自定义同步器的实现,同步状态表示锁被一个线程重复获取的次数。而读写锁的自定义同步器需要在同步状态上维护多个读线程和一个写线程的状态,所以该状态的设计成为实现读写锁的关键

ReentrantReadWriteLock 很好的利用了高低位。来实现一个整型控制两种状态的功能,读写锁将变量切分成了两个部分,高 16 位表示读锁的获取次数,低 16 位表示写锁的获取次数

它的特点:

  1. 写锁可以降级为读锁,但是读锁不能升级为写锁;
    1. 读锁里面加写锁,会导致死锁
    2. 写锁里面是可以加读锁的,这就是锁的降级
  2. 不管是 ReadLock 还是 WriteLock 都支持 等待可中断,语义与 ReentrantLock 一致;
  3. WriteLock 支持 Condition 并且与 ReentrantLock 语义一致,但 ReadLock 则不能使用 Condition,否则抛出 UnsupportedOperationException 异常;
  4. 默认构造方法为非公平模式 ,开发者也可以通过指定 fair 为 true 设置为公平模式

2. 精讲

1. 读写锁的概念

之前提到的锁(如:ReentrantLock)基本都是排他锁,即在同一时刻只允许一个线程进行访问,而读写锁在同一时刻可以允许多个读线程访问,但是在写线程访问时,所有的读线程和其他写线程均被阻塞。

读写锁维护了一对锁,一个读锁和一个写锁,通过分离读锁和写锁,使得并发性相比一般的排他锁有了很大的提升。

一般情况下,读写锁的性能都会比排它锁好,因为大多数场景读是多于写的。在读多于写的情况下,读写锁能够提供比排它锁更好的并发性和吞吐量。Java 并发包中提供读写锁的实现是ReentrantReadWriteLock,它提供的特性如下表所示:
在这里插入图片描述
但是事实上在 ReentrantReadWriteLock 里锁的实现是靠内部类 java.util.concurrent.locks.ReentrantReadWriteLock.Sync 完成的。这个类看起来比较眼熟,它是 AQS 的一个子类,这种类似的结构在 CountDownLatch、ReentrantLock、Semaphore 里面都存在。

在 ReentrantReadWriteLock 里面的锁主体就是一个 Sync,也就是 FairSync 或者 NonfairSync,所以说实际上只有一个锁,只是在获取读取锁和写入锁的方式上不一样而已。

【死磕Java并发】—–J.U.C之读写锁:ReentrantReadWriteLock

先看下 ReadWriteLock 接口的源码:

public interface ReadWriteLock {
       // 读锁Lock readLock(); // 写锁Lock writeLock();
}

ReentrantReadWriteLock 类是 ReadWriteLock 的实现类,它的整体结果如下:

public class ReentrantReadWriteLock implements ReadWriteLock, java.io.Serializable {
    private final ReentrantReadWriteLock.ReadLock readerLock;   private final ReentrantReadWriteLock.WriteLock writerLock;   final Sync sync;public ReentrantReadWriteLock() {
    this(false);   // 默认是非公平锁}// 公平锁和非公平锁在ReentrReentrantReadWriteLock初始化的时候就确定了public ReentrantReadWriteLock(boolean fair) {
    sync = fair ? new FairSync() : new NonfairSync();readerLock = new ReadLock(this);writerLock = new WriteLock(this);}// 实现父接口ReadWriteLock接口中的方法public ReentrantReadWriteLock.WriteLock writeLock() {
     return writerLock; }public ReentrantReadWriteLock.ReadLock  readLock()  {
     return readerLock; }// 静态内部类Sync,它的父类是AQSabstract static class Sync extends AbstractQueuedSynchronizer {
    // ...// 当别的线程获取读锁时,是否要阻塞abstract boolean readerShouldBlock();// 当别的线程获取写锁时,是否要阻塞abstract boolean writerShouldBlock();// ...}// 静态内部类NonfairSync,其父类是Syncstatic final class NonfairSync extends Sync {
    // ...}// 静态内部类FairSync,其父类是Syncstatic final class FairSync extends Sync {
    // ...}// 读锁,实现了Lock接口public static class ReadLock implements Lock, java.io.Serializable {
    private final Sync sync;// 构造器protected ReadLock(ReentrantReadWriteLock lock) {
    sync = lock.sync;}// ...}// 写锁,实现了Lock接口public static class WriteLock implements Lock, java.io.Serializable {
    private final Sync sync;// 构造器 protected WriteLock(ReentrantReadWriteLock lock) {
    sync = lock.sync;}// ...} // ...
}

从上面的的代码中可以发现,Sync 中提供了很多方法,但是只有两个方法是抽象的,也就是需要子类实现的。那我们就来看看它的子类 FairSync 和 NonFairSync 是如何实现这两个抽象方法的。

FairSync:

static final class FairSync extends Sync {
    private static final long serialVersionUID = -2274990926593161451L;final boolean writerShouldBlock() {
    return hasQueuedPredecessors();}final boolean readerShouldBlock() {
    return hasQueuedPredecessors();}
}

writerShouldBlockreaderShouldBlock 方法都表示当有别的线程也在尝试获取锁时,是否应该阻塞。

对于公平模式,hasQueuedPredecessors() 方法表示前面是否有等待线程。一旦前面有等待线程,那么为了遵循公平,当前线程也就应该被挂起

NonFairSync:

static final class NonfairSync extends Sync {
    private static final long serialVersionUID = -8159625535654395037L;final boolean writerShouldBlock() {
    return false; // writers can always barge}final boolean readerShouldBlock() {
    return apparentlyFirstQueuedIsExclusive();}
}

从上面可以看到,非公平模式下,writerShouldBlock 直接返回 false,说明不需要阻塞;而 readShouldBlock 调用了 apparentlyFirstQueuedIsExcluisve() 方法。该方法在当前线程是写锁占用的线程时,返回 true;否则返回 false。也就说明,如果当前有一个写线程正在写,那么该读线程应该阻塞

1. ReentrantReadWriteLock 的特性

  1. 获取顺序

    • 非公平模式(默认):当以非公平初始化时,读锁和写锁的获取的顺序是不确定的。非公平锁主张竞争获取,可能会延缓一个或多个读或写线程,但是会比公平锁有更高的吞吐量

    • 公平模式:当以公平模式初始化时,线程将会以队列的顺序获取锁。当当前线程释放锁后,等待时间最长的写锁线程就会被分配写锁;或者有一组读线程组等待时间比写线程长,那么这组读线程组将会被分配读锁。

      当有写线程持有写锁或者有等待的写线程时,一个尝试获取公平的读锁(非重入)的线程就会阻塞。这个线程直到等待时间最长的写锁获得锁后并释放掉锁后才能获取到读锁

  2. 可重入

    允许读锁和写锁可重入。写锁可以获得读锁和写锁,读锁不能获得写锁

  3. 锁降级

    允许写锁降低为读锁 ,但不支持读锁升级为写锁

  4. 中断锁的获取

    在读锁和写锁的获取过程中支持中断

  5. 支持 Condition

    写锁提供 Condition 实现

  6. 监控

    提供确定锁是否被持有等辅助方法

2. 读写锁的具体实现

下面分析 ReentrantReadWriteLock 的具体实现,主要包括:读写状态的设计、写锁的获取与释放、读锁的获取与释放以及锁降级。至于公平性与非公平性,在上篇文章重入锁 ReentrantLock 中已经讲解过了,本质上都是一样的,这里不再单独做区分的讲解了

1. 读锁状态的设计

读写锁同样依赖自定义同步器来实现同步功能,而读写状态就是其同步器的同步状态。回想 ReentrantLock 中自定义同步器的实现,同步状态表示锁被一个线程重复获取的次数。而读写锁的自定义同步器需要在同步状态(一个整型变量)上维护多个读线程和一个写线程的状态,使得该状态的设计成为读写锁实现的关键。

如果在一个整型变量上维护多种状态,就一定需要“按位切割使用”这个变量,读写锁将变量切分成了两个部分,高 16 位表示读,低 16 位表示写,划分方式如下图所示:
在这里插入图片描述
上图所示的当前同步状态表示一个线程已经获取了写锁,其重进入了 2 次,同时也连续获取了 2 次读锁。读锁和写锁通过位运算确定自己读和写的状态

2. 锁的获取

1. 写锁的获取

写锁是一个支持重进入的排它锁。如果当前线程已经获取了写锁,则增加写状态。如果当前线程在获取写锁时,读锁已经被获取(读锁状态不为 0)或者该线程不是已经获取写锁的线程,则当前线程进入等待状态

  1. 调用内部类 WriteLock 中的 lock() 方法

    public static class WriteLock implements Lock, java.io.Serializable {
          // ...// 获取写锁public void lock() {
          sync.acquire(1);  // 调用的是同步器AQS中的方法}// ...
    }
    
  2. 调用 AQS 中的 acquire 方法(因为 Sync 继承了 AQS 类,所有可以调用 AQS 中开放的方法)

    public final void acquire(int arg) {
          if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)){
          selfInterrupt();}         
    }
    
  3. 调用 Sync 中的 tryAcquire 方法

    public class ReentrantReadWriteLock implements ReadWriteLock, java.io.Serializable{
          // ...// 静态内部类Sync,它的父类是AQSabstract static class Sync extends AbstractQueuedSynchronizer {
          // ...protected final boolean tryAcquire(int acquires) {
          // 获取调用lock方法的当前线程Thread current = Thread.currentThread();// 获取当前线程的状态int c = getState();// 获取写锁的状态,写锁是排它锁int w = exclusiveCount(c);if (c != 0) {
          // 存在读锁或者当前获取线程不是已经获取写锁的线程,返回falseif (w == 0 || current != getExclusiveOwnerThread())return false;// 如果写锁的个数超过了最大值65535,抛出异常if (w + exclusiveCount(acquires) > MAX_COUNT)throw new Error("Maximum lock count exceeded");// 写入重入锁,返回truesetState(c + acquires);return true;}// 如果当前没有线程获得锁,如果写线程应该被阻塞或者CAS失败,返回falseif (writerShouldBlock() || !compareAndSetState(c, c + acquires))return false;// 否则将当前线程置为获得写锁的线程,返回truesetExclusiveOwnerThread(current);return true;}// ...}
    }
    

从代码和注释可以看到,获取写锁时有三步:

  1. 如果当前有写锁或者读锁。如果只有读锁,返回false,因为这时如果可以写,那么读线程得到的数据就有可能错误;如果只有写锁,但是线程不同,即不符合写锁重入规则,返回false ;

  2. 如果写锁的数量将会超过最大值65535,抛出异常;否则,写锁重入 ;

  3. 如果没有读锁或写锁的话,如果需要阻塞或者CAS失败,返回false;否则将当前线程置为获得写锁的线程

从上面可以看到调用了 writerShouldBlock 方法,FairSync 的实现是如果等待队列中有等待线程,则返回 false,说明公平模式下,只要队列中有线程在等待,那么后来的这个线程也是需要进入队列等待的;NonfairSync 中的直接返回的直接是 false,说明不需要阻塞。从上面的代码可以得出,当没有锁时,如果使用的非公平模式下的写锁的话,那么返回 false,直接通过 CAS 就可以获得写锁

2. 读锁的获取

读锁是一个支持重进入的共享锁,它能够被多个线程同时获取,在没有其他写线程访问时,读锁总是能够成功地被获取到,而所做的也只是增加读状态。

如果当前线程已经获取了读锁,则增加读状态。如果当前线程在获取读锁时,写锁已经被其他线程获取了,则进入等待状态

  1. 调用调用内部类 ReadLock 中的 lock() 方法

    public static class ReadLock implements Lock, java.io.Serializable {
          // ... // 获取读锁public void lock() {
          sync.acquireShared(1);  // 调用的是同步器AQS中的方法}// ...
    }
    
  2. 调用 AQS 中共享模式:acquireShared 方法

    if (tryAcquireShared(arg) < 0)doAcquireShared(arg);
    

    tryAcquireShared() 方法小于 0 时,那么会执行 doAcquireShared 方法将该线程加入到等待队列中。
    Sync 实现了 tryAcquireShared 方法,如下:

  3. 调用 Sync 中的 tryAcquireShared 方法

    public class ReentrantReadWriteLock implements ReadWriteLock, java.io.Serializable{
              // ...// 静态内部类Sync,它的父类是AQSabstract static class Sync extends AbstractQueuedSynchronizer {
          // ...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);// 如果读不应该阻塞并且读锁的个数小于最大值65535,并且可以成功更新状态值,成功if (!readerShouldBlock() && r < MAX_COUNT && compareAndSetState(c, c + SHARED_UNIT)) {
          // 如果当前读锁为0if (r == 0) {
          // 第一个读线程就是当前线程firstReader = current;firstReaderHoldCount = 1;}// 如果当前线程重入了,记录firstReaderHoldCountelse if (firstReader == current) {
          firstReaderHoldCount++;}// 当前读线程和第一个读线程不同,记录每一个线程读的次数else {
          HoldCounter rh = cachedHoldCounter;if (rh == null || rh.tid != getThreadId(current))cachedHoldCounter = rh = readHolds.get();else if (rh.count == 0)readHolds.set(rh);rh.count++;}return 1;}// 否则,循环尝试return fullTryAcquireShared(current);}        // ...}
    }
    

从上面的代码以及注释可以看出,获取读锁分为三步:

  1. 如果当前有写线程并且本线程不是写线程,那么失败,返回 -1;

  2. 否则,说明当前没有写线程或者本线程就是写线程(可重入),接下来判断是否应该读线程阻塞并且读锁的个数是否小于最大值,并且CAS成功使读锁+1,则成功,返回1。其余的操作主要是用于计数的;

  3. 如果2中失败了,失败的原因有三,第一是应该读线程应该阻塞;第二是因为读锁达到了上线;第三是因为CAS失败,有其他线程在并发更新state,那么会调动fullTryAcquireShared方法

fullTryAcquiredShared 方法如下:

final int fullTryAcquireShared(Thread current) {
    HoldCounter rh = null;for (;;) {
    int c = getState();// 一旦有别的线程获得了写锁,返回-1,失败if (exclusiveCount(c) != 0) {
    if (getExclusiveOwnerThread() != current)return -1;} // 如果读线程需要阻塞else if (readerShouldBlock()) {
    // Make sure we're not acquiring read lock reentrantlyif (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();}}if (rh.count == 0)return -1;}}// 如果读锁达到了最大值,抛出异常if (sharedCount(c) == MAX_COUNT)throw new Error("Maximum lock count exceeded");// 如果成功更改状态,成功返回if (compareAndSetState(c, c + SHARED_UNIT)) {
    if (sharedCount(c) == 0) {
    firstReader = current;firstReaderHoldCount = 1;} else if (firstReader == current) {
    firstReaderHoldCount++;} else {
    if (rh == null)rh = cachedHoldCounter;if (rh == null || rh.tid != getThreadId(current))rh = readHolds.get();else if (rh.count == 0)readHolds.set(rh);rh.count++;cachedHoldCounter = rh; // cache for release}return 1;}}
}

从上面可以看到 fullTryAcquireSharedtryAcquireShared 有很多类似的地方。 在上面可以看到多次调用了 readerShouldBlock 方法,对于公平锁,只要队列中有线程在等待,那么将会返回 true,也就意味着读线程需要阻塞;对于非公平锁,如果当前有线程获取了写锁,则返回 true。一旦不阻塞,那么读线程将会有机会获得读锁

3. 总结

  1. 如果当前没有写锁或读锁时,第一个获取锁的线程都会成功,无论该锁是写锁还是读锁;

  2. 如果当前已经有了读锁,那么这时获取写锁将失败,获取读锁有可能成功也有可能失败;

  3. 如果当前已经有了写锁,那么这时获取读锁或写锁,如果线程相同(可重入),那么成功;否则失败。

3. 锁的释放

获取锁要做的是更改 AQS 的状态值以及将需要等待的线程放入到队列中。释放锁要做的就是更改 AQS 的状态值以及唤醒队列中的等待线程来继续获取锁

1. 写锁的释放

  1. 调用 WriteLock 类中的 unlock 方法

    public void unlock() {
          sync.release(1);
    }
    
  2. 通过 Sync 调用 AQS 中的 relaease 方法

    public final boolean release(int arg) {
          if (tryRelease(arg)) {
          Node h = head;if (h != null && h.waitStatus != 0)unparkSuccessor(h);return true;}return false;
    }
    
  3. 调用 Sync 中的 tryRelease 方法

    一旦释放成功了,那么如果等待队列中有线程再等待,那么调用 unparkSuccessor 将下一个线程解除挂起。
    Sync 需要实现 tryRelease 方法。具体实现源码如下所示:

    public class ReentrantReadWriteLock implements ReadWriteLock, java.io.Serializable{
              ...// 静态内部类Sync,它的父类是AQSabstract static class Sync extends AbstractQueuedSynchronizer {
          ...        protected final boolean tryRelease(int releases) {
          // 如果没有线程持有写锁,但是仍要释放,抛出异常if (!isHeldExclusively())throw new IllegalMonitorStateException();int nextc = getState() - releases;boolean free = exclusiveCount(nextc) == 0;// 如果没有写锁了,那么将AQS的线程置为nullif (free)setExclusiveOwnerThread(null);// 更新状态setState(nextc);return free;}        ...}
    }
    

从上面可以看到,写锁的释放主要有三步:

  1. 如果当前没有线程持有写锁,但是还要释放写锁,抛出异常;

  2. 得到解除一把写锁后的状态,如果没有写锁了,那么将 AQS 的线程置为 null;

  3. 不管第二步中是否需要将 AQS 的线程置为 null,AQS 的状态总是要更新的

2. 读锁的释放

  1. 调用 ReadLock 中的 unlock 方法

    public void unlock() {
          sync.releaseShared(1);
    }
    
  2. 通过 Sync 调用 AQS 的 releaseShared 方法

    public final boolean releaseShared(int arg) {
          if (tryReleaseShared(arg)) {
          doReleaseShared();return true;}return false;
    }
    
  3. 调用 Sync 的 tryReleaseShared 方法,如果释放成功,调用 doReleaseShared 尝试唤醒下一个节点

    public class ReentrantReadWriteLock implements ReadWriteLock, java.io.Serializable{
              ...	// 静态内部类Sync,它的父类是AQSabstract static class Sync extends AbstractQueuedSynchronizer {
          ...        protected final boolean tryReleaseShared(int unused) {
          // 得到调用unlock的线程Thread current = Thread.currentThread();// 如果是第一个获得读锁的线程if (firstReader == current) {
          // assert firstReaderHoldCount > 0;if (firstReaderHoldCount == 1)firstReader = null;elsefirstReaderHoldCount--;}// 否则,是HoldCounter中计数-1else {
          HoldCounter rh = cachedHoldCounter;if (rh == null || rh.tid != getThreadId(current))rh = readHolds.get();int count = rh.count;if (count <= 1) {
          readHolds.remove();if (count <= 0)throw unmatchedUnlockException();}--rh.count;}// 死循环for (;;) {
          int c = getState();// 释放一把读锁int nextc = c - SHARED_UNIT;// 如果CAS更新状态成功,返回读锁是否等于0;失败的话,则重试if (compareAndSetState(c, nextc))return nextc == 0;}}        ...}
    }
    

    从上面可以看到,释放锁的第一步是更新 firstReaderHoldCounter 的计数,接下来进入死循环,尝试更新 AQS 的状态,一旦更新成功,则返回;否则,则重试。

    释放读锁对读线程没有影响,但是可能会使等待的写线程解除挂起开始运行。所以,一旦没有锁了,就返回 true,否则 false;返回 true 后,那么则需要释放等待队列中的线程,这时读线程和写线程都有可能再获得锁

3. 总结

  1. 如果当前是写锁被占有了,只有当写锁的数据降为 0 时才认为释放成功;否则失败。因为只要有写锁,那么除了占有写锁的那个线程,其他线程即不可以获得读锁,也不能获得写锁;

  2. 如果当前是读锁被占有了,那么只有在写锁的个数为 0 时才认为释放成功。因为一旦有写锁,别的任何线程都不应该再获得读锁了,除了获得写锁的那个线程

3. ReentrantReadWriteLock 中的其他方法

1. getOwner()

getOwner 方法用于返回当前获得写锁的线程,如果没有线程占有写锁,那么返回 null。实现如下:

protected Thread getOwner() {
    return sync.getOwner();
}

可以看到直接调用了 Sync 的 getOwner 方法,下面是 Sync 的 getOwner 方法:

final Thread getOwner() {
    // Must read state before owner to ensure memory consistencyreturn ((exclusiveCount(getState()) == 0) ? null : getExclusiveOwnerThread());
}

如果独占锁的个数为 0,说明没有线程占有写锁,那么返回 null;否则返回占有写锁的线程。

2. getReadLockCount()

getReadLockCount() 方法用于返回读锁的个数,实现如下:

public int getReadLockCount() {
    return sync.getReadLockCount();
}

可以看到调用了 Sync 中的 getReadLockCount() 方法:

final int getReadLockCount() {
    return sharedCount(getState());
}static int sharedCount(int c) {
    return c >>> SHARED_SHIFT; 
}

从上面代码可以看出,要想得到读锁的个数,就是看 AQS 的 state 的高 16 位。这和前面讲过的一样,高 16 位表示读锁的个数,低 16 位表示写锁的个数,通过位运算得到

3. getReadHoldCount()

getReadHoldCount() 方法用于返回当前线程所持有的读锁的个数,如果当前线程没有持有读锁,则返回 0。

public int getReadHoldCount() {
    return sync.getReadHoldCount();
}

可以看到,调用了 Sync 中的 getReadHoldCount() 方法:

final int getReadHoldCount() {
    // 如果没有读锁,自然每个线程都是返回0if (getReadLockCount() == 0)return 0;// 得到当前线程Thread current = Thread.currentThread();// 如果当前线程是第一个读线程,返回firstReaderHoldCount参数if (firstReader == current)return firstReaderHoldCount;// 如果当前线程不是第一个读线程,得到HoldCounter,返回其中的countHoldCounter rh = cachedHoldCounter;// 如果缓存的HoldCounter不为null并且是当前线程的HoldCounter,直接返回countif (rh != null && rh.tid == getThreadId(current))return rh.count;// 如果缓存的HoldCounter不是当前线程的HoldCounter,// 那么从ThreadLocal中得到本线程的HoldCounter,返回计数 int count = readHolds.get().count;// 如果本线程持有的读锁为0,从ThreadLocal中移除if (count == 0) readHolds.remove();return count;
}

从上面的代码中,可以看到两个熟悉的变量,firstReader 和 HoldCounter 类型。这两个变量在读锁的获取中接触过,前面没有细说,这里细说一下。HoldCounter 类的实现如下:

static final class HoldCounter {
    int count = 0;// Use id, not reference, to avoid garbage retentionfinal long tid = getThreadId(Thread.currentThread());
}

readHolds 是 ThreadLocalHoldCounter 类,定义如下:

static final class ThreadLocalHoldCounter extends ThreadLocal<HoldCounter> {
    public HoldCounter initialValue() {
    return new HoldCounter();}
}

可以看到,readHolds 存储了每一个线程的 HoldCounter,而 HoldCounter 中的 count 变量就是用来记录线程获得的写锁的个数。所以可以得出结论:Sync 维持总的读锁的个数,在 state 的高 16 位;由于读线程可以同时存在,所以每个线程还保存了获得的读锁的个数,这个是通过 HoldCounter 来保存的

除此之外,对于第一个读线程有特殊的处理,Sync 中有如下两个变量:

private transient Thread firstReader = null;
private transient int firstReaderHoldCount;

firstReader 表示第一个得到读锁的线程,firstReaderHoldCount 表示这个线程(第一个获取读锁的线程)获得读锁的重入数。所以可以得出结论:第一个获取到读锁的信息保存在 firstReader 中;其余获取到读锁的线程(即不是第一个获取读锁的线程用 HoldCount 记录)的信息保存在 HoldCounter 中。

看完了 HoldCounter 和 firstReader,再来看一下 getReadLockCount 的实现,主要有三步:

  1. 当前没有读锁,那么自然每一个线程获得的读锁都是 0;

  2. 如果当前线程是第一个获取到读锁的线程,那么返回 firstReadHoldCount;

  3. 如果当前线程不是第一个获取到读锁的线程,得到该线程的 HoldCounter,然后返回其 count 字段。如果 count 字段为 0,说明该线程没有占有读锁,那么从 readHolds 中移除。获取 HoldCounter 分为两步,第一步是与 cachedHoldCounter 比较,如果不是,则从 readHolds 中获取。

4. getWriteHoldCount()

getWriteHoldCount() 方法返回当前线程所持有写锁的个数

public int getWriteHoldCount() {
    return sync.getWriteHoldCount();
}

接着调用了 Sync 中的 getWriteHoldCount() 方法:

final int getWriteHoldCount() {
    return isHeldExclusively() ? exclusiveCount(getState()) : 0;
}

可以看到如果没有线程持有写锁,那么返回 0;否则返回 AQS 的 state 的低 16 位

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1uiJ99XX-1588761971211)(4. Java 并发(13)- ReentrantLock与ReentrantReadWriteLock.assets/1578062708277.png)]

4. 读写锁总结

当分析 ReentranctReadWriteLock 时,或者说分析内部使用 AQS 实现的工具类时,需要明白的就是 AQS 的 state 代表的是什么。ReentrantLockReadWriteLock 中的 state 同时表示写锁和读锁的个数。为了实现这种功能,state 的高 16 位表示读锁的个数,低 16 位表示写锁的个数。

AQS 有两种模式:共享模式和独占模式,读写锁的实现中,读锁使用共享模式;写锁使用独占模式;

另外一点需要记住,当有读锁时,写锁就不能获得;而当有写锁时,除了获得写锁的这个线程可以获得读锁外,其他线程不能获得读锁

  相关解决方案