ReentrantReadWriteLock 最佳实践

3 阅读7分钟

ReentrantReadWriteLock 最佳实践

以下是在实际开发中使用该锁的最佳实践建议,涵盖设计决策、性能优化、常见陷阱规避等方面。


1. 明确适用场景:读多写少

最佳实践:仅在读操作远多于写操作的场景下使用读写锁。

  • 适用示例:配置中心、缓存系统、路由表、字典数据、树形结构的统计查询。
  • 不适用示例:写操作超过 30% 的场景(如频繁更新的计数器、实时日志写入)。此时读写锁的 CAS 开销可能超过其收益,甚至比独占锁更慢。

判断标准:可以通过性能测试或监控评估读写比例。如果写比例 > 30%,考虑使用 ConcurrentHashMapCopyOnWriteArrayList 或无锁数据结构。


2. 优先使用非公平模式(默认)

最佳实践:除非有明确的公平性需求,否则使用非公平锁(默认构造函数)。

  • 原因:非公平锁吞吐量更高(比公平锁高 20%~40%),大多数业务对锁获取的绝对公平不敏感。
  • 何时使用公平锁
    • 写线程必须及时执行,避免饥饿(例如实时交易系统)。
    • 读线程不应对写线程造成无限延迟(但公平锁仍可能因读线程持续到来而延迟写线程,需结合其他策略)。

3. 严格控制读锁持有时间

最佳实践:读锁临界区应尽量短小,避免在持有读锁时执行耗时操作(如 I/O、远程调用、复杂计算)。

  • 原因:读锁会阻塞写锁,长时间持有读锁会导致写线程饥饿,降低系统吞吐量和响应性。
  • 改进方法
    • 将耗时操作移出读锁临界区,仅用读锁保护必要的共享变量读取。
    • 如果必须读取大量数据,考虑使用“拷贝”模式:在读锁内快速复制数据到本地变量,然后释放读锁再处理副本。
    • 对于极高并发读场景,考虑使用 StampedLock 的乐观读。

示例(反模式)

// 错误:在读锁内执行耗时操作
readLock.lock();
try {
    Data data = sharedData;
    Thread.sleep(1000); // 模拟耗时
    process(data);
} finally {
    readLock.unlock();
}

改进后

Data data;
readLock.lock();
try {
    data = sharedData;
} finally {
    readLock.unlock();
}
// 耗时操作在锁外执行
Thread.sleep(1000);
process(data);

4. 正确使用锁降级,避免锁升级

最佳实践:在“写后立即读”且需要保证数据一致性的场景下,使用锁降级;绝对不要尝试锁升级(读锁内获取写锁)。

  • 锁降级标准模式

    writeLock.lock();
    try {
        // 修改共享数据
        data = newData;
        // 降级:获取读锁
        readLock.lock();
    } finally {
        writeLock.unlock(); // 释放写锁,保留读锁
    }
    try {
        // 使用最新数据执行只读操作(此时其他读线程可并发)
        useData(data);
    } finally {
        readLock.unlock();
    }
    
  • 锁升级死锁示例(禁止)

    readLock.lock();
    try {
        // ...
        writeLock.lock(); // 死锁!当前线程等待自己释放读锁
    } finally { ... }
    
  • 注意事项:降级后务必在 finally 中释放读锁,否则写线程永久阻塞。


5. 使用 tryLock() 避免无限等待

最佳实践:在可能发生锁竞争或需要避免死锁的场景下,使用 tryLock()tryLock(long, TimeUnit) 替代 lock()

  • 适用场景
    • 需要获取多个锁时,使用 tryLock 按顺序尝试并回退,避免死锁。
    • 写操作可以容忍失败时(如缓存更新失败后可重试或降级)。
    • 防止读锁长时间持有导致的写线程饥饿:写锁使用 tryLock 带超时,若超时则记录日志或采取其他措施。

示例(多锁顺序避免死锁)

ReentrantReadWriteLock lock1 = ...;
ReentrantReadWriteLock lock2 = ...;

if (lock1.writeLock().tryLock()) {
    try {
        if (lock2.writeLock().tryLock()) {
            try {
                // 安全操作
            } finally {
                lock2.writeLock().unlock();
            }
        }
    } finally {
        lock1.writeLock().unlock();
    }
}

6. 避免读锁内调用未知代码(回调)

最佳实践:在持有读锁期间,不要调用可能获取其他锁(尤其是写锁)的外部方法,也不要调用可能阻塞的代码。

  • 原因:外部方法可能尝试获取写锁(例如回调函数中更新数据),导致死锁。
  • 解决方案:在读锁临界区内仅做简单的状态读取,所有可能产生副作用的调用移到锁外。

7. 使用 StampedLock 替代读写锁以获得更高性能(当不可重入可接受时)

最佳实践:在追求极致读性能且不需要锁重入的场景下,优先考虑 StampedLock

  • 对比

    特性ReentrantReadWriteLockStampedLock
    可重入
    条件变量写锁支持不支持
    乐观读
    读多写少吞吐量更高(乐观读无锁)
    API 复杂度简单复杂(需验证戳)
  • 典型 StampedLock 乐观读模式

    StampedLock sl = new StampedLock();
    long stamp = sl.tryOptimisticRead();
    Data data = sharedData;
    if (!sl.validate(stamp)) {
        stamp = sl.readLock();
        try {
            data = sharedData;
        } finally {
            sl.unlockRead(stamp);
        }
    }
    process(data);
    
  • 迁移建议:如果现有代码使用 ReentrantReadWriteLock 且没有重入需求,可以重构为 StampedLock 以提升性能。


8. 合理选择锁粒度

最佳实践:避免使用单一的全局读写锁保护大对象,考虑拆分为多个独立的读写锁(分段锁)。

  • 示例:一个大型缓存可以按 key 的哈希值分段,每段使用独立的读写锁,减少竞争。
  • ConcurrentHashMap 对比ConcurrentHashMap 已经实现了高效的分段锁(或 CAS),通常优于自己实现分段读写锁。仅在需要更复杂的读-写语义(如全局统计、批量写)时才自行实现。

9. 监控与调优

最佳实践:在生产环境中监控读写锁的竞争情况,必要时调整锁策略。

  • 监控指标
    • 读锁和写锁的平均等待时间。
    • 读锁和写锁的排队线程数。
    • 锁降级发生的频率。
  • 工具:使用 JConsole、VisualVM 或通过 AQS 提供的 getQueueLength()getReadLockCount() 等方法定期采集。
  • 调优方向
    • 如果写锁等待时间过长,考虑降低读锁持有时间或改用 StampedLock
    • 如果写线程饥饿,考虑切换到公平锁或使用 tryLock 超时机制。

10. 避免在 finally 块之外释放锁

最佳实践:始终在 finally 块中释放锁,确保即使发生异常也能释放。

lock.lock();
try {
    // 业务逻辑
} finally {
    lock.unlock();
}

反例:在 try 块末尾释放锁(若中间抛异常,锁永远不会释放)。


11. 注意重入次数限制

最佳实践:避免深度递归或循环中重复获取读锁/写锁超过 65535 次。

  • 原因state 的低 16 位和高 16 位分别限制了写锁和读锁总重入次数(最大 65535)。虽然正常情况下很难达到,但应避免代码错误导致无限重入(如递归中未正确释放锁)。

12. 文档化锁的使用约定

最佳实践:在代码注释中明确说明哪些字段受读锁保护,哪些受写锁保护,以及锁的获取顺序(如果有多个锁)。

/**
 * 缓存容器。
 * - 读锁保护所有读操作(get, size, containsKey)
 * - 写锁保护所有写操作(put, remove, clear)
 * - 不允许在持有读锁时获取写锁(禁止升级)
 */
private final ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();

总结:最佳实践速查表

实践项核心原则反模式
适用场景读多写少(读比例 > 70%)写操作频繁
公平性默认非公平随意使用公平锁降低性能
读锁持有时间尽量短小,耗时操作移出临界区读锁内做 I/O 或复杂计算
锁降级写后读使用降级,禁止升级读锁内获取写锁(死锁)
避免等待使用 tryLock 带超时盲目使用 lock() 可能导致死锁
重入限制避免递归过深忽略 65535 上限
锁释放finally 块释放try 内释放
性能优化考虑 StampedLock固守读写锁不评估
监控采集等待队列长度不监控锁竞争
文档注释锁保护范围无文档说明

遵循以上最佳实践,可以充分发挥 ReentrantReadWriteLock 的优势,避免常见的性能陷阱和并发错误。