ReentrantReadWriteLock 深度解析

它解决什么问题

有个缓存,99% 的时间在读,1% 的时间在写。

用 synchronized?读操作互相阻塞,吞吐量上不去。

用 Semaphore?Semaphore 是计数信号量,不是锁,没法区分读写。

ReentrantReadWriteLock 就是来解决这个的:读多写少场景下的高性能锁

读锁是共享的,多个线程可以同时读。写锁是互斥的,一次只能有一个线程写。


核心原理

读写锁分离的核心思想:

复制代码
读操作:可以多个线程同时读(读读并发)
写操作:只能一个线程写,并且读写互斥(写写互斥、读写互斥)

一个缓存的数据结构:

java 复制代码
class Cache {
    private final Map<String, Object> map = new HashMap<>();
    private final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
    private final Lock readLock = rwl.readLock();
    private final Lock writeLock = rwl.writeLock();

    Object get(String key) {
        readLock.lock();
        try {
            return map.get(key);
        } finally {
            readLock.unlock();
        }
    }

    void put(String key, Object value) {
        writeLock.lock();
        try {
            map.put(key, value);
        } finally {
            writeLock.unlock();
        }
    }
}

源码解读

构造方法

java 复制代码
public ReentrantReadWriteLock() {
    this(false);  // 默认非公平
}

public ReentrantReadWriteLock(boolean fair) {
    sync = fair ? new FairSync() : new NonfairSync();
    readerLock = new ReadLock(this);
    writerLock = new WriteLock(this);
}

公平和非公平的区别跟 ReentrantLock 一样:公平模式会检查等待队列。

Sync 内部结构

java 复制代码
static final int SHARED_SHIFT   = 16;
static final int SHARED_UNIT    = (1 << SHARED_SHIFT);  // 0x00010000
static final int MAX_COUNT      = (1 << SHARED_SHIFT) - 1;  // 65535
static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1;  // 0x0000FFFF

// state 被拆成两半
// 高16位:读锁计数(共享模式)
// 低16位:写锁计数(独占模式)

JDK 用一个 int 的 state,分成两半来存读和写:

复制代码
32位 state = [读锁计数:16位] [写锁计数:16位]

读锁获取

java 复制代码
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);  // 当前读锁数量
    if (!readerShouldBlock() &&  // 公平模式要检查队列
        r < MAX_COUNT) {
        // CAS 尝试加读锁
        if (compareAndSetState(c, c + SHARED_UNIT)) {
            // 成功,标记第一个获取读锁的线程
            if (r == 0) {
                firstReader = current;
                firstReaderHoldCount = 1;
            } else {
                // 不是第一个,更新计数
                cachedHoldCounter = readHolds.get();
                readHolds.set(3);
            }
            return 1;
        }
    }
    return fullTryAcquireShared(current);
}

核心:CAS 加 SHARED_UNIT(65536),高16位加一。

写锁获取

java 复制代码
protected final boolean tryAcquire(int acquires) {
    Thread current = Thread.currentThread();
    int c = getState();
    int w = exclusiveCount(c);  // 当前写锁计数
    if (c != 0) {
        // 有读锁或有其他线程拿着写锁
        if (w == 0 || current != getExclusiveOwnerThread()) {
            return false;
        }
        // 重入:当前线程已经拿了写锁
        if (w + exclusiveCount(acquires) > MAX_COUNT) {
            throw new Error("Maximum lock count exceeded");
        }
    }
    // CAS 尝试加写锁
    if (compareAndSetState(c, c + acquires)) {
        setExclusiveOwnerThread(current);
        return true;
    }
    return false;
}

关键点:写锁和读锁互斥,只要有读锁存在,写锁就无法获取(除非是同一个线程重入)。

锁降级

java 复制代码
public void processData() {
    readLock.lock();  // 先拿读锁
    try {
        // 读取数据
        if (needUpdate) {
            // 要更新了,先拿写锁
            writeLock.lock();  // 写锁降级:读锁还在手里
            try {
                // 更新数据
                needUpdate = false;
            } finally {
                writeLock.unlock();  // 释放写锁,但读锁还在
            }
        }
        // 用新数据
    } finally {
        readLock.unlock();  // 最后释放读锁
    }
}

这个场景很有意思:读锁不释放的情况下获取写锁,再释放写锁。这就是"锁降级",防止其他线程在更新期间读取到不一致的数据。


踩坑经历

坑1:读锁情况下获取写锁会死锁

java 复制代码
readLock.lock();
try {
    writeLock.lock();  // ❌ 这里会一直阻塞!
    // 永远不会走到这里
} finally {
    writeLock.unlock();
    readLock.unlock();
}

ReentrantReadWriteLock 不支持锁升级。读锁还没释放,又去拿写锁,写锁和读锁互斥,所以写锁永远拿不到,程序卡死。

坑2:忘记读多写少的前提

我之前在写多读少的场景下用了读写锁,结果性能反而更差。原因是读写锁的开销比普通互斥锁大,如果写操作占比高,读写分离的优势体现不出来。

教训

  • 读多写少 → 用读写锁
  • 写多读少 → 用普通互斥锁
  • 读写各半 → 随便哪个都行,读写锁优势不明显

常见面试题

Q1:ReentrantReadWriteLock 的实现原理?

用一个 int 的 state 分成两半:高16位存读锁计数,低16位存写锁计数。读锁是共享模式,多线程 CAS 加;写锁是独占模式。

Q2:读锁和写锁是如何实现互斥的?

tryAcquireShared 里会检查是否有写锁被占用,如果有就直接返回失败。tryAcquire 里检查是否有读锁或写锁被占用,有的话也返回失败。

Q3:什么是锁降级?

持有写锁的情况下获取读锁称为锁降级。作用是保证在更新数据时不会有其他线程读取到旧数据,更新完成后释放写锁降级为读锁。

Q4:锁升级可以吗?

不行。持有读锁时获取写锁会死锁,因为写锁需要等其他所有读锁释放,但读锁持有者又不会主动释放。

Q5:StampedLock 和 ReentrantReadWriteLock 的区别?

StampedLock 使用戳(stamp)而不是计数,性能更好。支持乐观读模式:先读,发现冲突再升级为悲观读。ReentrantReadWriteLock 不支持乐观读。


总结

记住三点:

  1. state 分两半:高16位读锁计数,低16位写锁计数
  2. 读写互斥:读锁和写锁不能同时存在
  3. 锁降级 OK,锁升级死锁:常见踩坑点

面试问到这,直接把 state 分段设计的源码逻辑讲出来,面试官会对你刮目相看。

相关推荐
铭keny1 小时前
子系统 SSO 单点登录接入配置指南
java
电商API_180079052471 小时前
淘宝商品评论数据获取指南|批量自动化|api应用
java·爬虫·spring·性能优化·自动化
梦梦代码精2 小时前
Likeshop一个开源商城到底有哪些功能模块?
java·低代码·开源·php
赏金术士2 小时前
Kotlin 从入门到进阶 之协程 Flow 模块(九)
开发语言·kotlin·php
java1234_小锋2 小时前
Spring AI 2.0 开发Java Agent智能体 - 对话与提示词工程(Prompt)
java·人工智能·spring
赵钰老师2 小时前
R语言在生态环境领域中的应用
开发语言·数据分析·r语言
guygg882 小时前
四旋翼无人机串级PID控制MATLAB仿真
开发语言·matlab·无人机
guygg882 小时前
四足液压机器人设计程序MATLAB实现
开发语言·matlab·机器人
Frank_refuel2 小时前
C++之STL->string类的使用和实现
java·开发语言·c++