【JUC】读写锁

Java多线程中的读写锁(ReadWriteLock)

一、为什么需要读写锁?

普通的互斥锁(如 synchronized 或 ReentrantLock)无论读还是写都会加锁,导致读读之间也会互斥,性能低下。

但实际场景中,读操作远多于写操作,而且:

  • 多个线程同时读不会产生数据竞争 → 应该允许并发
  • 读和写、写和写之间才需要互斥

读写锁正是为此设计的:读读共享、读写互斥、写写互斥。


二、核心接口

java 复制代码
public interface ReadWriteLock {
    Lock readLock();   // 获取读锁
    Lock writeLock();  // 获取写锁
}

主要实现类:ReentrantReadWriteLock


三、ReentrantReadWriteLock 的规则

组合 是否允许
读 + 读 ✅ 允许(共享)
读 + 写 ❌ 互斥
写 + 写 ❌ 互斥
写 + 读 ❌ 互斥

关键特性:

  1. 可重入:读锁和写锁都支持重入。但同一线程持有写锁后可以再获取读锁(锁降级),反之不行(读锁不能升级为写锁,会死锁)。
  2. 公平性可选 :构造函数可传入 fair 参数。
    • 非公平(默认):可能造成读线程饥饿,但吞吐量高。
    • 公平:按队列顺序,避免饥饿,但性能略低。
java 复制代码
ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
ReentrantReadWriteLock fairLock = new ReentrantReadWriteLock(true);

四、基础使用示例

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

    // 读操作
    public Object get(String key) {
        readLock.lock();
        try {
            return map.get(key);
        } finally {
            readLock.unlock();
        }
    }

    // 写操作
    public void put(String key, Object value) {
        writeLock.lock();
        try {
            map.put(key, value);
        } finally {
            writeLock.unlock();
        }
    }
}

注意: 必须在 finally 中释放锁,避免异常导致锁泄漏。


五、锁降级(Lock Downgrade)

指持有写锁时获取读锁,然后释放写锁,从而让其他线程能读。这是允许的。

java 复制代码
writeLock.lock();
try {
    // 修改数据
    map.put("k", "v");

    readLock.lock();   // 获取读锁(降级)
} finally {
    writeLock.unlock(); // 释放写锁
}
try {
    // 此时持有读锁,其他读线程可并发
    Object v = map.get("k");
} finally {
    readLock.unlock();
}

为什么需要降级? 保证当前线程在修改后能立即读到自己的修改,同时不阻塞其他读线程,并防止其他写线程插入。


六、锁升级(不允许!)

java 复制代码
readLock.lock();
// ... 读操作
writeLock.lock(); // ❌ 死锁!多个线程同时持有读锁时会互相等待

因为读锁是共享的,如果多个线程都持有读锁并试图升级为写锁,就会互相等待对方释放读锁,导致死锁。所以 ReentrantReadWriteLock 不支持锁升级。


七、内部原理简析

ReentrantReadWriteLock 内部基于 AQS(AbstractQueuedSynchronizer) 实现:

  • state 高 16 位表示读锁持有数 ,低 16 位表示写锁持有数。
  • 写锁:独占模式,类似 ReentrantLock。
  • 读锁:共享模式,用 firstReader 和 cachedHoldCounter 优化记录每个线程的读锁重入次数。

因为读写各有 16 位,所以最多支持 65535 个读锁或写锁重入次数。


八、适用场景与注意事项

适合:

  • 读多写少的场景,如缓存、配置中心、字典查询。
  • 需要保证数据一致性,但读操作并发度要求高。

不适合:

  • 写操作非常频繁 → 读写锁的维护开销反而拖慢性能。
  • 临界区非常短 → 直接用 synchronized 或 ReentrantLock 可能更快。

注意事项:

  1. 读锁不支持 Condition,只有写锁可以 newCondition()。
  2. 不要在持有读锁时调用 writeLock().lock()。
  3. 锁释放必须放在 finally。
  4. 非公平模式下,读线程可能长期获取不到锁(写线程排队时,新来的读线程会直接尝试抢锁)。

九、与 StampedLock 的简单对比(Java 8+)

StampedLock 是更快的读写锁,支持乐观读:

java 复制代码
StampedLock sl = new StampedLock();
long stamp = sl.tryOptimisticRead(); // 乐观读,不加锁
// ... 读数据
if (!sl.validate(stamp)) {           // 校验是否有写操作
    stamp = sl.readLock();           // 退化为悲观读
    try { /* 读 */ } finally { sl.unlockRead(stamp); }
}
  • StampedLock 不可重入 ,不支持 Condition,但性能更高。
  • 适合读多写少且对吞吐量要求极高的场景。

十、总结一句话

读写锁通过分离读锁和写锁,实现 读读并发、读写/写写互斥 ,在读多写少 的场景下显著提升并发性能;使用时要警惕锁升级死锁,并优先考虑是否需要 StampedLock 这类更高效的替代方案。

相关推荐
niucloud-admin1 小时前
NIUCLOUD LITE AI开发文档--基础开发规范
开发语言·php
小羊没烦恼!1 小时前
T-SQL查询高级—SQL Server索引中的碎片和填充因子
开发语言·前端·javascript·数据库·后端·python·web安全
yuzhiboyouye1 小时前
后端怎么评估工作量
java
java资料站1 小时前
十五、Spring AI Alibaba · 高级 ·Graph
java·人工智能·spring
纪念 2291 小时前
C++ vector(一)
开发语言·c++
benchmark_cc2 小时前
实时行情系统如何避免无意义的重复请求?从请求去重到缓存设计
大数据·开发语言·python·pandas·量化交易·股票数据·quantdash
田里的水稻2 小时前
Python_两大法宝dir() 与 help() 的妙用
开发语言·python
郝学胜-神的一滴2 小时前
Python浏览器自动化:Selenium与Playwright实战对比,网页爬虫&自动操作一站式指南
开发语言·爬虫·python·selenium·程序人生·自动化
金銀銅鐵2 小时前
[Java] 用 GUI 展示 ArrayList 扩容的过程
java·后端·ai编程