ReentrantReadWriteLock 详解

67 阅读4分钟

ReentrantReadWriteLock是Java中基于AQS的读写分离锁,支持可重入、公平模式及锁降级,通过读写分离机制优化读多写少场景的并发性能,需避免锁升级引发死锁。

一、核心特性

ReentrantReadWriteLock 是 Java 并发包中基于 ReadWriteLock 接口的实现类,提供 读写分离的锁机制,适用于 读多写少 的场景。其核心特性如下:

特性说明
读写分离允许多个线程同时持有读锁(共享锁),但写锁(独占锁)只能被一个线程持有。
可重入性读锁和写锁均支持重入(同一线程可重复获取锁)。
公平性选择支持公平模式(按请求顺序分配锁)和非公平模式(允许插队)。
锁降级允许线程在持有写锁时获取读锁,再释放写锁,实现从写锁到读锁的降级。
锁升级限制不支持锁升级(持有读锁时不能直接获取写锁,需先释放读锁)。

二、实现原理

ReentrantReadWriteLock 基于 AbstractQueuedSynchronizer (AQS) 实现,通过一个 state 变量(int 类型)的高 16 位和低 16 位分别管理读锁和写锁的状态。

  1. 状态拆分

    • 高 16 位:记录读锁的持有次数(包括重入次数)。
    • 低 16 位:记录写锁的持有次数(写锁是独占的)。
  2. 读锁(ReadLock)

    • 共享模式:多个线程可同时获取读锁。

    • 实现逻辑

      protected int tryAcquireShared(int unused) {
          // 检查是否有写锁被持有(非当前线程)
          if (exclusiveCount(getState()) != 0 && getExclusiveOwnerThread() != currentThread)
              return -1; // 获取失败
          // 通过 CAS 增加读锁计数
          // ...
      }
      
  3. 写锁(WriteLock)

    • 独占模式:同一时间只能有一个线程持有写锁。

    • 实现逻辑

      protected boolean tryAcquire(int unused) {
          // 检查是否有其他线程持有读锁或写锁
          if (getState() != 0) {
              if (currentThread != getExclusiveOwnerThread())
                  return false; // 获取失败
          }
          // 通过 CAS 设置写锁状态
          // ...
      }
      
  4. 锁降级(Write → Read Lock)

    • 步骤

      1. 获取写锁。
      2. 修改共享数据。
      3. 获取读锁(防止其他写线程修改数据)。
      4. 释放写锁。
      5. 后续操作使用读锁保证数据可见性。
    • 示例

      writeLock.lock();
      try {
          // 修改数据
          readLock.lock(); // 锁降级
      } finally {
          writeLock.unlock();
      }
      try {
          // 读取数据(其他写线程无法修改)
      } finally {
          readLock.unlock();
      }
      

三、使用场景

场景说明
缓存系统读操作频繁,写操作较少(如缓存数据的加载和更新)。
配置管理多线程读取配置,偶尔更新配置(需保证更新时的独占性)。
数据统计高频读取统计数据,低频更新统计结果。
数据库连接池管理连接时,允许多线程获取连接(读),但扩容或缩容时需独占锁(写)。

四、性能与注意事项

  1. 适用场景限制

    • 读多写少:读写锁在写操作频繁时性能可能不如普通互斥锁(如 ReentrantLock)。
    • 线程竞争激烈:非公平模式在高并发场景下可能导致线程饥饿。
  2. 避免锁升级

    • 持有读锁时尝试获取写锁会导致死锁,必须按顺序释放读锁后再获取写锁:

      readLock.lock();
      try {
          // 读操作
          readLock.unlock(); // 必须先释放读锁
          writeLock.lock();  // 再获取写锁
          // 写操作
      } finally {
          writeLock.unlock();
      }
      
  3. 公平模式 vs 非公平模式

    • 公平模式:按请求顺序分配锁,避免饥饿,但吞吐量低。
    • 非公平模式:允许插队,吞吐量高,但可能导致某些线程长期等待。

五、代码示例(缓存系统)

public class Cache<K, V> {
    private final Map<K, V> map = new HashMap<>();
    private final ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
    private final Lock readLock = rwLock.readLock();
    private final Lock writeLock = rwLock.writeLock();
​
    // 读缓存
    public V get(K key) {
        readLock.lock();
        try {
            return map.get(key);
        } finally {
            readLock.unlock();
        }
    }
​
    // 写缓存
    public void put(K key, V value) {
        writeLock.lock();
        try {
            map.put(key, value);
        } finally {
            writeLock.unlock();
        }
    }
​
    // 锁降级示例:更新并读取数据
    public void updateAndRead(K key, V value) {
        writeLock.lock();
        try {
            // 修改数据
            map.put(key, value);
            // 锁降级
            readLock.lock();
        } finally {
            writeLock.unlock();
        }
​
        try {
            // 读取数据(其他线程无法修改)
            System.out.println(map.get(key));
        } finally {
            readLock.unlock();
        }
    }
}

六、对比其他锁机制

锁类型优势劣势
synchronized语法简单,自动释放锁。无法实现读写分离,性能较低。
ReentrantLock支持公平锁、可中断锁。读写操作均需互斥,不适合读多写少场景。
ReentrantReadWriteLock读写分离,提高读性能。写锁饥饿问题(公平模式下可缓解)。

总结

ReentrantReadWriteLock 通过读写分离显著优化了读多写少场景的性能,但其正确使用需注意以下要点:

  1. 锁降级是安全操作,但锁升级必须避免。
  2. 在写操作频繁的场景下,优先考虑普通互斥锁。
  3. 合理选择公平模式与非公平模式,平衡吞吐量与公平性。

通过合理应用读写锁,可以在保证线程安全的同时,最大化系统的并发处理能力。