ReentrantReadWriteLock · JUC

ReentrantReadWriteLock 源码分析

读多写少的场景下,独占锁把互不冲突的读线程也串行化了。ReentrantReadWriteLock 用 AQS 的一个 state 同时管两把锁:读锁共享、写锁独占,写锁还能”降级”成读锁。核心技巧是把 state 按位切成两半,一次 CAS 同时维护读写两个计数。

版本注意:大家常说的「高 16 位读计数、低 16 位写计数」是 JDK 8~24 的 int state 布局(各 16 位,最大计数 65535)。
本地 JDK 25 源码已把 Sync 换成 AbstractQueuedLongSynchronizer,state 是 long,切分点从 16 变成 32,MAX_COUNT 提升到 Integer.MAX_VALUE——位运算和整个机制一字不差,只是每半边更宽了。下面代码以本地源码为准。

// 基于 JDK 25 (本地 D:/1ForCode/JAVA_Source), java.util.concurrent.locks.ReentrantReadWriteLock
abstract static class Sync extends AbstractQueuedLongSynchronizer {   // JDK ≤24 继承的是 int 版 AQS

    static final int SHARED_SHIFT   = 32;                    // JDK ≤24 这里是 16
    static final long SHARED_UNIT    = (1L << SHARED_SHIFT); // 读计数 +1 = state 加这一个单位
    static final long MAX_COUNT      = Integer.MAX_VALUE;    // JDK ≤24 是 (1<<16)-1 = 65535
    static final long EXCLUSIVE_MASK = (1L << SHARED_SHIFT) - 1;

    // 高半段 = 总读锁计数(所有线程加起来), 低半段 = 写锁重入次数(只可能属于一个线程)
    static int sharedCount(long c)    { return (int)(c >>> SHARED_SHIFT); }
    static int exclusiveCount(long c) { return (int)(c & EXCLUSIVE_MASK); }

    // state 里只有"总读计数", 每个线程各自重入了几次, 得靠 ThreadLocal 单独记
    static final class HoldCounter {
        int count;          // initially 0
        // Use id, not reference, to avoid garbage retention
        final long tid = LockSupport.getThreadId(Thread.currentThread());  // 存 tid 不存 Thread 引用, 防止 ThreadLocal 值反向拽住线程对象
    }

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

    private transient ThreadLocalHoldCounter readHolds;      // 兜底: 每线程读重入计数
    private transient HoldCounter cachedHoldCounter;         // 缓存"最后一个成功拿读锁的线程"的计数器, 省一次 ThreadLocal 查找; 非 volatile, 良性数据竞争
    private transient Thread firstReader;                    // 把读计数从 0 变 1 的那个线程, 单读者场景连 ThreadLocal 都不用碰
    private transient int firstReaderHoldCount;

    abstract boolean readerShouldBlock();                    // 公平/非公平唯一的分歧点: 明明能抢, 要不要让
    abstract boolean writerShouldBlock();

    // 写锁获取: 独占语义, 走 AQS 的 tryAcquire
    protected final boolean tryAcquire(long acquires) {
        Thread current = Thread.currentThread();
        long c = getState();
        long w = exclusiveCount(c);
        if (c != 0) {
            // (Note: if c != 0 and w == 0 then shared count != 0)
            if (w == 0 || current != getExclusiveOwnerThread())
                return false;               // w==0 即有读锁在场 → 写锁必须等; 哪怕读锁是自己持有的也一样失败 —— 这就是"锁升级"会死锁的代码根源
            if (w + exclusiveCount(acquires) > MAX_COUNT)
                throw new Error("Maximum lock count exceeded");
            // Reentrant acquire
            setState(c + acquires);         // 写锁重入, 只有自己能到这, 普通写即可
            return true;
        }
        if (writerShouldBlock() ||          // c==0 锁全空: 公平版还要看队列, 非公平版直接抢
            !compareAndSetState(c, c + acquires))
            return false;
        setExclusiveOwnerThread(current);
        return true;
    }

    protected final boolean tryRelease(long releases) {
        if (!isHeldExclusively())
            throw new IllegalMonitorStateException();
        long nextc = getState() - releases;
        boolean free = exclusiveCount(nextc) == 0;  // 只看低半段: 降级场景下高半段还挂着自己的读计数, 写锁照样能放
        if (free)
            setExclusiveOwnerThread(null);
        setState(nextc);
        return free;
    }
}
// 基于 JDK 25 (本地 D:/1ForCode/JAVA_Source), java.util.concurrent.locks.ReentrantReadWriteLock.Sync
// 读锁获取: 共享语义, 走 AQS 的 tryAcquireShared, 返回 >=0 表示成功
protected final long tryAcquireShared(long unused) {
    Thread current = Thread.currentThread();
    long c = getState();
    if (exclusiveCount(c) != 0 &&
        getExclusiveOwnerThread() != current)
        return -1L;                     // 别人持写锁 → 读失败; 写锁是自己的则放行 —— 锁降级的入口就在这个条件里
    int r = sharedCount(c);
    if (!readerShouldBlock() &&
        r < MAX_COUNT &&
        compareAndSetState(c, c + SHARED_UNIT)) {   // 整个 long 一起 CAS, 读计数 +1 的同时天然校验了"写计数没变"
        if (r == 0) {
            firstReader = current;      // 第一个读者: 最快路径, 零 ThreadLocal 开销
            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);      // 缓存里是自己但 ThreadLocal 已被 remove 过, 补回去
            rh.count++;
        }
        return 1L;
    }
    return fullTryAcquireShared(current);   // 快路径失败(该让/CAS 输/计数满), 转完整版重试循环
}

final long fullTryAcquireShared(Thread current) {
    HoldCounter rh = null;
    for (;;) {
        long c = getState();
        if (exclusiveCount(c) != 0) {
            if (getExclusiveOwnerThread() != current)
                return -1;
            // else we hold the exclusive lock; blocking here
            // would cause deadlock.          // 持写锁的自己若在这被挡, 就没人能放写锁了 → 降级必须无条件放行
        } 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 != LockSupport.getThreadId(current)) {
                        rh = readHolds.get();
                        if (rh.count == 0)
                            readHolds.remove();   // 白查了一次, 立刻清掉, 别给线程留一个空 HoldCounter
                    }
                }
                if (rh.count == 0)
                    return -1L;               // 首次获取且政策要求让路 → 老实进队
            }
        }
        if (sharedCount(c) == MAX_COUNT)
            throw new Error("Maximum lock count exceeded");
        if (compareAndSetState(c, c + SHARED_UNIT)) {
            // ... 与 tryAcquireShared 相同的 firstReader/cachedHoldCounter 记账, 略
            return 1L;
        }
    }
}

protected final boolean tryReleaseShared(long unused) {
    Thread current = Thread.currentThread();
    if (firstReader == current) {
        // assert firstReaderHoldCount > 0;
        if (firstReaderHoldCount == 1)
            firstReader = null;
        else
            firstReaderHoldCount--;
    } else {
        HoldCounter rh = cachedHoldCounter;
        if (rh == null ||
            rh.tid != LockSupport.getThreadId(current))
            rh = readHolds.get();
        int count = rh.count;
        if (count <= 1) {
            readHolds.remove();               // 重入数归零就从 ThreadLocal 移除, 防内存泄漏
            if (count <= 0)
                throw unmatchedUnlockException();   // 没拿过读锁却来解锁
        }
        --rh.count;
    }
    for (;;) {
        long c = getState();
        long nextc = c - SHARED_UNIT;
        if (compareAndSetState(c, nextc))     // 多个读者并发释放, 必须 CAS 自旋(对比写锁释放的普通写)
            // 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;                // 只有读写全空才返回 true 去唤醒后继 —— 唤醒的通常是等疯了的写者
    }
}
// 基于 JDK 25 (本地 D:/1ForCode/JAVA_Source), java.util.concurrent.locks.ReentrantReadWriteLock
static final class NonfairSync extends Sync {
    final boolean writerShouldBlock() {
        return false; // writers can always barge      // 写者永远可以插队
    }
    final boolean readerShouldBlock() {
        /* As a heuristic to avoid indefinite writer starvation,
         * block if the thread that momentarily appears to be head
         * of queue, if one exists, is a waiting writer. ... */
        return apparentlyFirstQueuedIsExclusive();     // 队头等着的是写者 → 新读者让路。防写饥饿的启发式, 不是保证
    }
}

static final class FairSync extends Sync {
    final boolean writerShouldBlock() {
        return hasQueuedPredecessors();                // 公平版读写一视同仁: 前面有人排队就不抢
    }
    final boolean readerShouldBlock() {
        return hasQueuedPredecessors();
    }
}

原理串讲

一次典型的读锁获取:readLock().lock()sync.acquireShared(1)tryAcquireShared
它先看低半段 exclusiveCount(c):有写锁且不是自己,直接返回 -1 去 AQS 排队;否则问 readerShouldBlock(),不用让就 compareAndSetState(c, c + SHARED_UNIT)——注意 CAS 的期望值是完整的 state,所以读计数 +1 的瞬间同时确认了写计数没被别人改过,这是位切分的第一个”为什么”:两个计数塞进一个原子变量,读写之间的互斥判断和计数更新合并成一次 CAS,不需要额外的锁去保护”两个计数的一致性”。
快路径失败(CAS 竞争输了、或政策说该让、或重入场景)才落到 fullTryAcquireShared 的完整循环,把重入判断从快路径里剥出去,是因为绝大多数读获取是非重入的,不值得让热路径每次都查 ThreadLocal。

state 高半段只是”全体读者的总数”,回答不了”我这个线程重入了几次”——但 unlock() 必须校验你真的持有,getReadHoldCount() 也要按线程报数。
于是有了三级记账:firstReader 记第一个读者(单读者场景一次 ThreadLocal 都不碰),cachedHoldCounter 记最后一个成功获取的读者(获取和释放通常是同一个线程紧挨着发生,缓存命中率很高),都不命中才查 readHolds 这个 ThreadLocal。
HoldCounter 存 tid 而不存 Thread 引用、计数归零就 readHolds.remove(),两个细节都是在防 ThreadLocal 场景的内存滞留。

锁降级(持写 → 取读 → 放写)为什么被放行:tryAcquireShared 的失败条件是「有写锁 持有者不是自己」,自己持写时取读锁畅通无阻;随后 tryRelease 只检查 exclusiveCount(nextc) == 0,高半段挂着自己的读计数完全不妨碍写锁释放。
反过来锁升级(持读 → 取写)必然卡死:tryAcquirec != 0 && w == 0 意味着有读锁在场,不管这读锁是不是自己的都返回 false,线程进 AQS 队列 park,等读计数清零——可它自己那份读计数永远等不到自己来释放。
更糟的是两个读者同时升级,互相等对方放读锁,谁也醒不来。为什么不特判”读锁全是我自己的就允许升级”?因为升级必须是原子的排他动作,两个线程同时符合”特判”就会同时等对方,JUC 干脆在语义上禁止,把死锁风险从运行时提前到设计期。

写饥饿问题落在 readerShouldBlock 上。读锁是共享的,只要读者源源不断,state 高半段永远非零,写者的 tryAcquire 永远看到 c != 0 && w == 0
非公平模式的解法是 apparentlyFirstQueuedIsExclusive():新读者抢锁前瞄一眼队头,如果队头等着的是写者就主动让路,让写者有机会插进读者流里。
这只是概率性缓解——写者排在几个读者后面时新读者照样插队。公平模式则读写都走 hasQueuedPredecessors(),严格 FIFO,写者不会饿死,代价是读吞吐明显下降。

设计取舍

  • 一个 state 切两半,换来读写计数的单次 CAS 原子更新;代价是每半边计数上限缩水(JDK ≤24 只有 65535,JDK 25 换 long 后放宽到 Integer.MAX_VALUE)。
  • 禁止锁升级不是实现偷懒,是语义上避免”两个升级者互等”的必然死锁;需要升级的场景应该先放读锁再抢写锁,并接受中间窗口数据可能已变。
  • 锁降级的价值:写完后继续持读,保证自己刚写的数据在读期间不被别的写者改掉,又尽早把写锁让出去恢复读并发。
  • firstReader/cachedHoldCounter 是典型的”为常见路径加缓存”:单读者和”谁获取谁释放”两种模式覆盖了绝大多数用法,靠良性数据竞争省掉 ThreadLocal 查找。
  • 非公平模式写者可插队 + 队头写者优先的启发式,是吞吐和写饥饿之间的折中;真读多写少到极致(读者永不断流),该考虑 StampedLock 的乐观读而不是调公平参数。

延伸阅读