ReentrantReadWriteLock 深度解析

🤔 道格·李为什么要区分读锁和写锁

ReentrantLock 是可重入的互斥锁——无论什么操作,同一时刻只有一个线程能持有锁。这在写多读少的场景里不是问题,但在读多写少的场景(缓存查询、配置读取、字典加载)中,ReentrantLock 带来了不必要的串行化:100 个线程同时读,它们不应该互相阻塞,因为读取不修改数据。

道格·李在设计 JSR 166 时专门为此引入了读写锁。ReentrantReadWriteLock 把锁拆成两把:

  • 读锁(readLock)——共享锁,多个读线程可以同时持有,读与读不互斥
  • 写锁(writeLock)——独占锁,写线程独占,写与写、写与读互斥

这个设计基于一个观察:大多数并发访问的数据结构是"读多写少"的。如果 90% 的操作都是读取,那么互斥锁让这 90% 的操作全部串行化——白白浪费了并发能力。读写锁让读操作完全并行,只在写操作发生时短暂阻塞。

内部实现上,ReentrantReadWriteLock 把 AQS 的 32 位 state 字段拆成高 16 位(记录共享的读锁持有次数)和低 16 位(记录独占的写锁重入次数),用一个 int 同时跟踪两种模式的持有状态。这也是为什么读写锁在同一时刻要么被读线程共享、要么被写线程独占——两种模式共享同一个 state,状态机决定了它们互斥。

📋 基本用法:readLock 和 writeLock

ReentrantReadWriteLock 提供两个 Lock 对象:

类型获取方法AQS 模式
readLock()共享锁readLock.lock()共享模式(Shared)
writeLock()独占锁writeLock.lock()独占模式(Exclusive)
ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
Lock readLock = rwLock.readLock();
Lock writeLock = rwLock.writeLock();

// 读锁:多线程可同时持有
readLock.lock();
try {
    // 读取操作
} finally {
    readLock.unlock();
}

// 写锁:只有一个线程能持有
writeLock.lock();
try {
    // 写入操作
} finally {
    writeLock.unlock();
}

读写互斥规则

stateDiagram-v2
    state "无锁" as NOLOCK
    state "读锁持有中" as READ
    state "写锁持有中" as WRITE

    NOLOCK --> READ : 任意线程获取读锁
    NOLOCK --> WRITE : 任意线程获取写锁
    READ --> READ : 其他线程获取读锁(允许)
    READ --> WRITE : 任意线程获取写锁(阻塞等待)
    WRITE --> READ : 任意线程获取读锁(阻塞等待)
    WRITE --> WRITE : 任意线程获取写锁(阻塞等待)
    READ --> NOLOCK : 所有读锁释放
    WRITE --> NOLOCK : 写锁释放
当前状态请求读锁请求写锁
无锁允许允许
读锁持有中允许(读-读不互斥)阻塞(读-写互斥)
写锁持有中阻塞(写-读互斥)阻塞(写-写互斥)

关键点 :写锁释放后,如果有读线程和写线程同时在等待,谁先获取取决于锁的公平性设置。非公平模式下,写线程可能被插队的读线程持续阻塞(写饥饿问题)。

📐 state 的拆分设计:一个 int 存两种状态

为什么需要拆分 state

ReentrantLock 只需一个 state 就能表达锁状态:0 = 空闲,1 = 锁定,>1 = 重入。但 ReentrantReadWriteLock 需要同时表达两种锁的状态:多个线程各自持有一份读锁,同时有一个线程持有写锁。一个 int 变量如何表达这些信息?

答案是 按位拆分 :将 32 位 int 拆成高 16 位和低 16 位。

flowchart TD
%% 半暗底色 + 高亮描边:完美适配博客深色/浅色双主题 %%
classDef process fill:#1e1e24,stroke:#6b7280,stroke-width:2px,color:#e5e7eb;
    subgraph STATE["AQS state(32位 int)"]
        HIGH["高 16 位\n读锁持有计数\n(所有线程读锁重入次数的总和)"]
        LOW["低 16 位\n写锁重入计数\n(持有写锁的线程的重入次数)"]
    end
    HIGH --- LOW

class HIGH,LOW,STATE process;

源码中的常量定义和拆分操作:

// ReentrantReadWriteLock.Sync
static final int SHARED_SHIFT   = 16;
static final int SHARED_UNIT    = (1 << SHARED_SHIFT);  // 65536
static final int MAX_COUNT      = (1 << SHARED_SHIFT) - 1;  // 65535
static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1;  // 0x0000FFFF

// 获取读锁计数:state 右移 16 位
static int sharedCount(int c)    { return c >>> SHARED_SHIFT; }

// 获取写锁计数:state 与低 16 位掩码
static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; }
操作公式示例
获取读锁计数state >>> 16state=65537 → 读计数=1, 写计数=1
获取写锁计数state & 0x0000FFFFstate=131072 → 读计数=2, 写计数=0
读锁+1state + (1 << 16)CAS: compareAndSetState(c, c + SHARED_UNIT)
写锁+1state + 1setState(c + 1)(与 ReentrantLock 一致)

关键字段解释

  • SHARED_UNIT = 65536 = 2^16。读锁每获取一次,state 增加 65536,相当于高 16 位 +1
  • EXCLUSIVE_MASK = 0x0000FFFF。用这个掩码过滤 state,得到低 16 位(写锁计数)
  • sharedCount(c)>>> 是无符号右移,保证高位填 0

为什么用 16 位分割

16 位能表达的最大值是 65535。这意味着:

  • 写锁最多重入 65535 次(实际不会触达)
  • 读锁每个线程的持有计数也受 ThreadLocal 的 HoldCounter 约束

写锁的获取与释放

写锁是 独占模式 ,逻辑与 ReentrantLock 类似,但需要额外检查读锁是否被持有:

// ReentrantReadWriteLock.Sync
protected final boolean tryAcquire(int acquires) {
    Thread current = Thread.currentThread();
    int c = getState();
    int w = exclusiveCount(c);        // ① 获取写锁计数(低 16 位)

    if (c != 0) {                     // ② state != 0,锁已被持有
        if (w == 0 || current != getExclusiveOwnerThread())
            return false;             // ③ 有读锁(w=0, c!=0)或者写锁不是自己的 → 失败
        if (w + acquires > MAX_COUNT)
            throw new Error("Maximum lock count exceeded");
        setState(c + acquires);       // ④ 写锁重入:state + 1
        return true;
    }

    // ⑤ c == 0,无锁状态
    if (writerShouldBlock() ||        // ⑥ 公平/非公平策略判断
        !compareAndSetState(c, c + acquires))
        return false;                 // ⑦ CAS 失败或策略要求阻塞 → 入队
    setExclusiveOwnerThread(current); // ⑧ CAS 成功,设置写锁持有者
    return true;
}

写锁获取失败的两种情况

  1. c != 0 && w == 0:state 非零但写锁计数为 0 → 有线程持有读锁 → 写锁必须等待所有读锁释放
  2. c == 0writerShouldBlock() 返回 true → 入队等待
protected final boolean tryRelease(int releases) {
    if (!isHeldExclusively())
        throw new IllegalMonitorStateException();
    int nextc = getState() - releases;         // ① state - 1
    boolean free = exclusiveCount(nextc) == 0; // ② 写锁计数归零
    if (free)
        setExclusiveOwnerThread(null);         // ③ 清除持有者
    setState(nextc);                           // ④ 更新 state
    return free;
}

写锁释放与 ReentrantLock 一致。free = true 时 AQS 会唤醒后继节点。

读锁的获取与释放

读锁是 共享模式 ,核心复杂度在于需要记录每个线程的读锁重入次数:

// ReentrantReadWriteLock.Sync
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);  // ② 当前读锁总计数

    // ③ 快速路径:CAS 增加读锁计数
    if (!readerShouldBlock() &&          // 公平/非公平策略
        r < MAX_COUNT &&
        compareAndSetState(c, c + SHARED_UNIT)) {
        // ... 更新 HoldCounter(见下文)
        return 1;
    }
    // ④ CAS 失败或需要阻塞 → 走完整版本
    return fullTryAcquireShared(current);
}

读锁获取成功的条件

  • 没有写锁被其他线程持有(写锁持有者是当前线程时允许 —— 这是锁降级的基础)
  • readerShouldBlock() 返回 false
flowchart TD
%% 半暗底色 + 高亮描边:完美适配博客深色/浅色双主题 %%
classDef data fill:#052e16,stroke:#16a34a,stroke-width:2px,color:#bbf7d0,font-weight:bold;
classDef condition fill:#2a1147,stroke:#a855f7,stroke-width:2px,color:#ede9fe,font-weight:bold;
classDef process fill:#1e1e24,stroke:#6b7280,stroke-width:2px,color:#e5e7eb;
classDef startEnd fill:#701a4c,stroke:#e11d48,stroke-width:2.5px,color:#fce7f3,font-weight:bold;
    A[tryAcquireShared] --> B{写锁被持有?}
    B -->|否| C{readerShouldBlock?}
    B -->|是| B2{持有者是当前线程?}
    B2 -->|是| C
    B2 -->|否| FAIL[返回 -1\n入队等待]
    C -->|否| D[CAS state + SHARED_UNIT]
    C -->|是| E[fullTryAcquireShared]
    D -->|成功| F[更新 HoldCounter\n返回 1]
    D -->|失败| E
    E -->|成功| F
    E -->|失败| FAIL

class B,B2,C condition;
class A,E data;
class D process;
class F,FAIL startEnd;

📝 HoldCounter:记录每个线程的读锁重入次数

读锁被多个线程同时持有,但 AQS 的 state 只有总计数。需要单独的数据结构记录每个线程各自的读锁重入次数:

// ReentrantReadWriteLock.Sync
static final class HoldCounter {
    int count;          // 该线程的读锁重入次数
    final long tid = LockSupport.getThreadId(Thread.currentThread());
}

static final class ThreadLocalHoldCounter
    extends ThreadLocal<HoldCounter> {
    public HoldCounter initialValue() {
        return new HoldCounter();
    }
}
// 快速路径中的 HoldCounter 更新(在 tryAcquireShared 中)
if (r == 0) {                    // 第一个读线程
    firstReader = current;
    firstReaderHoldCount = 1;
} else if (firstReader == current) {
    firstReaderHoldCount++;      // 第一个读线程重入
} else {
    HoldCounter rh = cachedHoldCounter;
    if (rh == null || rh.tid != LockSupport.getThreadId(current))
        cachedHoldCounter = rh = readHolds.get();  // ThreadLocal 获取
    else if (rh.count == 0)
        readHolds.set(rh);
    rh.count++;                  // 重入计数 +1
}

三层读锁重入计数缓存体系

道格·李(Doug Lea)为了追求极致性能,设计了三层递进的缓存来记录每个线程的读锁重入次数,逐层降级避免昂贵的 ThreadLocal 查找:

flowchart TD
%% 半暗底色 + 高亮描边:完美适配博客深色/浅色双主题 %%
classDef process fill:#1e1e24,stroke:#6b7280,stroke-width:2px,color:#e5e7eb;
classDef condition fill:#2a1147,stroke:#a855f7,stroke-width:2px,color:#ede9fe,font-weight:bold;
classDef data fill:#052e16,stroke:#16a34a,stroke-width:2px,color:#bbf7d0,font-weight:bold;
    A[线程获取读锁\n需记录重入次数] --> B{是第一个读线程?}
    B -->|是| L1["🏆 第一层: firstReader + firstReaderHoldCount\n(Sync 对象上的两个普通字段)"]
    B -->|否| C{与上一读线程相同?}
    C -->|是| L2["🥈 第二层: cachedHoldCounter\n(单条目缓存,指针引用)"]
    C -->|否| L3["🥉 第三层: readHolds ThreadLocal\n(完整的 ThreadLocal.get 查找)"]
    L1 --> RESULT[count++]
    L2 --> RESULT
    L3 --> RESULT

class B,C condition;
class L2 data;
class A,L1,L3,RESULT process;
层级缓存结构存储位置命中条件开销
🥇 第一层firstReader + firstReaderHoldCountSync 对象字段当前线程是全局第一个获取读锁的线程2 次字段读写,无 CAS,无 ThreadLocal
🥈 第二层cachedHoldCounterSync 对象字段当前线程与上一次获取读锁的线程相同1 次引用比较 + 1 次 tid 比较
🥉 第三层readHolds(ThreadLocalHoldCounter)Thread → ThreadLocalMap前两层全部未命中ThreadLocal.get() → ThreadLocalMap 线性探测查找

第一层:firstReader / firstReaderHoldCount

绝大多数情况下,第一个读线程就是唯一的读线程(没有并发读),或至少是获取读锁频率最高的线程。这两个字段直接挂在 Sync 对象上,绕过所有缓存查找:

// Sync 对象上的字段(非 volatile,单线程写入)
private transient Thread firstReader;
private transient int firstReaderHoldCount;

命中时只需 2 次直接字段赋值,不需要进入 ThreadLocalMap。firstReader 不是 volatile 的,因为只有持有读锁的线程在 tryAcquireShared 中写入,不存在多线程竞争写的问题。

第二层:cachedHoldCounter

如果 firstReader 不命中,检查 cachedHoldCounter 是否指向当前线程的 HoldCounter。多数业务代码中,读锁的连续获取发生在同一个线程内(例如 for 循环中多次 readLock.lock()),因此上一个获取读锁的线程很大概率就是当前线程:

// Sync 对象上的字段
private transient HoldCounter cachedHoldCounter;

// 在 tryAcquireShared 中:
HoldCounter rh = cachedHoldCounter;
if (rh == null || rh.tid != LockSupport.getThreadId(current))
    cachedHoldCounter = rh = readHolds.get();  // 未命中,降级到第三层
else if (rh.count == 0)
    readHolds.set(rh);  // rh 之前被 remove 过,重新 set 回 ThreadLocal
rh.count++;

cachedHoldCounter 的关键细节:当 rh.tid 匹配成功但 rh.count == 0 时,意味着该线程之前释放过读锁,其 HoldCounter 已从 ThreadLocal 中 remove(读锁释放到 0 时会调用 readHolds.remove())。此时需要重新 readHolds.set(rh) 将 HoldCounter 放回当前线程的 ThreadLocalMap 中。

第三层:readHolds(ThreadLocalHoldCounter)

前两层全部不命中时,才执行 readHolds.get() — 这是一个完整的 ThreadLocal.get() 调用,内部需要计算 0x61c88647 黄金分割哈希索引,在 ThreadLocalMap 中做线性探测查找:

private transient ThreadLocalHoldCounter readHolds;

// ThreadLocal.get() 内部的调用链:
// readHolds.get()
//   → Thread.threadLocals(ThreadLocalMap)
//     → Entry[(threadLocalHashCode & (len-1))](哈希定位)
//       → 线性探测(最多 n 次比较)

三层命中率分析

flowchart TD
%% 半暗底色 + 高亮描边:完美适配博客深色/浅色双主题 %%
classDef process fill:#1e1e24,stroke:#6b7280,stroke-width:2px,color:#e5e7eb;
classDef data fill:#052e16,stroke:#16a34a,stroke-width:2px,color:#bbf7d0,font-weight:bold;
classDef reject fill:#450a0a,stroke:#dc2626,stroke-width:2px,color:#fecaca,font-weight:bold;
    subgraph CASE1[场景1:单线程读]
        S1[Thread-1 反复 readLock] --> S1L1[每次都命中第一层\nfirstReader=Thread-1]
    end
    subgraph CASE2[场景2:交替读]
        S2T1[Thread-1 readLock] --> S2L1[第一层命中]
        S2T2[Thread-2 readLock] --> S2L3[前两层未命中\n降级第三层]
        S2T2_2[Thread-2 再次 readLock] --> S2L2[第二层命中\ncachedHoldCounter=Thread-2]
    end
    subgraph CASE3[场景3:高并发读]
        S3[多线程各自一次 readLock] --> S3L3[全部降级第三层]
    end

class S2L2 data;
class CASE1,CASE2,CASE3,S1,S1L1,S2L1,S2T1,S2T2,S2T2_2,S3 process;
class S2L3,S3L3 reject;

在实际业务中,场景1和场景2占绝大多数,前两层缓存命中率极高。道格·李通过这三个字段,将读锁重入计数的平均开销从一次完整的 ThreadLocal.get()(含线性探测)降到了 2 次普通字段访问。

读锁释放时,将 HoldCounter.count 递减,同时对 state 执行 CAS(c - SHARED_UNIT)。当 sharedCount 归零时,AQS 会唤醒后继的写线程。

⬇️ 锁降级:从写锁降到读锁

锁降级 (Lock Downgrade)是指在持有写锁的情况下,先获取读锁,再释放写锁的过程:

public void processData() {
    writeLock.lock();
    try {
        // 修改数据
        cache.put("key", computeValue());
        // 获取读锁(在写锁释放之前)
        readLock.lock();
    } finally {
        // 先释放写锁
        writeLock.unlock();
    }
    // 现在只持有读锁
    try {
        // 读取数据(其他读线程也可同时读)
        return cache.get("key");
    } finally {
        readLock.unlock();
    }
}
sequenceDiagram
    participant T1 as 线程T1
    participant WL as 写锁
    participant RL as 读锁
    participant T2 as 其他读线程

    T1->>WL: writeLock.lock()
    Note over T1: 修改数据...
    T1->>RL: readLock.lock()(锁降级开始)
    Note over T1: 同时持有写锁和读锁
    T1->>WL: writeLock.unlock()
    Note over T1: 只持有读锁

    T2->>RL: readLock.lock()
    Note over T2: 可以获取读锁(读-读不互斥)

为什么需要锁降级 :持有写锁修改数据后,希望后续的读取能看到刚写入的数据。如果先释放写锁再获取读锁,中间可能被其他写线程插入修改,导致读取到的不一致数据。锁降级保证了 写 → 读的原子性过渡

锁降级在源码中的支撑 :读锁的 tryAcquireShared 中有这一判断:

if (exclusiveCount(c) != 0 &&
    getExclusiveOwnerThread() != current)
    return -1;

exclusiveCount(c) != 0 表示有写锁,但 getExclusiveOwnerThread() == current 表示写锁是当前线程持有的。此时读锁 不阻塞 ,允许获取。这是锁降级可以执行的根本原因。

⬆️ 为什么不能锁升级

🔒 锁升级 (Lock Upgrade)是指在持有读锁的情况下,尝试获取写锁。 ReentrantReadWriteLock 不支持锁升级 ,尝试这样做会导致死锁:

// 这段代码会导致死锁!
readLock.lock();
try {
    // ... 读操作
    writeLock.lock();  // 永久阻塞!读锁未释放,写锁永远等不到
    try {
        // ... 永远不会到达
    } finally {
        writeLock.unlock();
    }
} finally {
    readLock.unlock();
}

死锁原因

sequenceDiagram
    participant T1 as 线程T1
    participant RL as 读锁
    participant WL as 写锁
    participant T2 as 线程T2

    T1->>RL: readLock.lock()
    T2->>RL: readLock.lock()
    Note over T1,T2: T1和T2都持有读锁

    T1->>WL: writeLock.lock()
    Note over T1: 阻塞!等待所有读锁释放

    T2->>RL: readLock.unlock()
    Note over T1: T2释放了读锁,但T1自己也持有读锁\nT1如果不释放读锁,写锁永远等不到
    Note over T1: T1在等写锁获取→写锁在等T1释放读锁→死锁

T1 自己持有读锁,同时尝试获取写锁。但写锁要求所有读锁释放才能获取——包括 T1 自己的读锁。而 T1 正在 writeLock.lock() 中阻塞,不会去释放读锁。这就是典型的 自己阻塞自己的死锁

操作是否支持原因
锁降级 (写→读)支持持有写锁的线程可以安全获取读锁,释放写锁后只留读锁
锁升级 (读→写)不支持读锁未释放时尝试获取写锁,要求所有读锁释放(包括自己),导致死锁

📐 公平模式与非公平模式

// 非公平(默认)
ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();

// 公平
ReentrantReadWriteLock fairLock = new ReentrantReadWriteLock(true);

两者的核心差异在两个方法中体现:

// 非公平版本:读锁不阻塞(除非队列头是写线程,防止写饥饿)
final boolean readerShouldBlock() {
    return apparentlyFirstQueuedIsExclusive();  // ① 队列头是写线程时阻塞
}

// 公平版本:队列中有等待者就阻塞
final boolean readerShouldBlock() {
    return hasQueuedPredecessors();  // ② 严格 FIFO
}

// 非公平版本:写锁从不阻塞(极易抢到)
final boolean writerShouldBlock() {
    return false;  // ③ 永远不阻塞 → 非公平
}

// 公平版本:写锁也检查是否有前驱
final boolean writerShouldBlock() {
    return hasQueuedPredecessors();  // ④ 严格 FIFO
}
维度非公平模式公平模式
读锁阻塞条件队列头部是写线程队列中有任何等待者
写锁阻塞条件永不阻塞(直接 CAS)队列中有任何等待者
吞吐量
写饥饿风险有(大量读线程可能让写线程持续等待)
默认

🛠️ 日常开发中的常用方法

方法用途频率
readLock().lock() / unlock()获取/释放读锁
writeLock().lock() / unlock()获取/释放写锁
readLock().tryLock(timeout, unit)带超时的读锁尝试
writeLock().tryLock(timeout, unit)带超时的写锁尝试
getReadLockCount()获取当前持有读锁的线程数近似值
isWriteLocked()查询写锁是否被持有
// 场景1:带超时的缓存更新
public Object getOrCompute(String key) throws InterruptedException {
    // 先尝试读
    readLock.lock();
    try {
        Object val = cache.get(key);
        if (val != null) return val;
    } finally {
        readLock.unlock();
    }

    // 缓存未命中,尝试写
    if (!writeLock.tryLock(500, TimeUnit.MILLISECONDS)) {
        throw new TimeoutException("获取写锁超时");
    }
    try {
        // 双重检查(其他线程可能已写入)
        Object val = cache.get(key);
        if (val != null) return val;
        val = computeValue(key);
        cache.put(key, val);
        return val;
    } finally {
        writeLock.unlock();
    }
}

// 场景2:锁降级确保数据一致性
public Map<String, Object> snapshot() {
    writeLock.lock();
    try {
        // 更新数据
        cache.put("version", incrementVersion());
        // 锁降级:获取读锁后再释放写锁
        readLock.lock();
    } finally {
        writeLock.unlock();
    }
    // 只持有读锁,安全返回快照
    try {
        return new HashMap<>(cache);
    } finally {
        readLock.unlock();
    }
}

🎯 总结

flowchart TD
%% 半暗底色 + 高亮描边:完美适配博客深色/浅色双主题 %%
classDef process fill:#1e1e24,stroke:#6b7280,stroke-width:2px,color:#e5e7eb;
classDef reject fill:#450a0a,stroke:#dc2626,stroke-width:2px,color:#fecaca,font-weight:bold;
classDef highlight fill:#431407,stroke:#ea580c,stroke-width:2px,color:#fed7aa,font-weight:bold;
    subgraph WHY[为什么需要]
        W1[ReentrantLock 读操作也互斥\n读多写少场景性能差]
        W2[需要读-读不互斥\n读-写互斥的锁]
    end
    subgraph HOW[如何实现]
        H1[state 高16位=读锁计数\n低16位=写锁重入]
        H2[读锁=共享模式 AQS\n写锁=独占模式 AQS]
        H3[HoldCounter ThreadLocal\n记录每线程读锁重入]
    end
    subgraph RULE[核心规则]
        R1[读-读: 不互斥]
        R2[读-写: 互斥]
        R3[写-写: 互斥]
        R4[锁降级: 支持\n写→读的原子过渡]
        R5[锁升级: 不支持\n会导致死锁]
    end
    WHY --> HOW
    HOW --> RULE

class RULE highlight;
class H1,H2,H3,HOW,R1,R2,R3,R5,W1,W2,WHY process;
class R4 reject;
核心问题答案
为什么需要 ReentrantReadWriteLock读多写少场景下,ReentrantLock 将读操作串行化。读写锁允许多线程同时读,只在写时互斥
state 如何表达两种状态高 16 位存储读锁总计数(state>>>16),低 16 位存储写锁重入计数(state&0xFFFF
为什么不能锁升级持有读锁时尝试获取写锁,写锁要求所有读锁释放(包括自己持有的),自己阻塞自己,形成死锁
锁降级为什么安全写锁持有者获取读锁时,tryAcquireShared 专门允许写锁持有者获取读锁,不阻塞
写饥饿如何缓解非公平模式下 readerShouldBlock() 检查队列头是否是写线程,是则让读线程入队,给写线程机会
读锁重入次数存在哪HoldCounter + ThreadLocal,记录每个线程各自的读锁重入次数。firstReadercachedHoldCounter 做缓存优化