面试题:你了解 Java 中的读写锁吗?

是的,Java 中的读写锁(ReadWriteLock)是一种并发控制机制,它允许多个线程同时读取共享资源,但写操作必须独占锁。这种设计在“读多写少”的场景下能显著提高并发性能。以下是关于 Java 中读写锁的详细解析:


1. 读写锁的核心概念

  • 读锁(Read Lock):共享锁,允许多个线程同时获取读锁,用于读取共享资源。
  • 写锁(Write Lock):排他锁,同一时间只能有一个线程持有写锁,用于修改共享资源。
  • 核心特性
    • 读写互斥:写锁和读锁/写锁互斥(写操作期间不允许其他线程读或写)。
    • 写写互斥:写操作必须独占锁。
    • 读读共享:多个线程可以同时读取资源。

2. Java 中的实现类

Java 提供了两个主要的读写锁实现:

(1) ReentrantReadWriteLock

  • 特点
    • 支持重入性(读锁和写锁均可重入)。
    • 支持公平锁非公平锁(默认非公平)。
    • 锁降级:写锁可以降级为读锁(但读锁不能升级为写锁)。
    • 通过 AQS(AbstractQueuedSynchronizer)实现状态管理。
  • 适用场景
    • 缓存系统、配置中心等读多写少的场景。
    • 需要重入性或锁降级的业务逻辑。

(2) StampedLock(Java 8 引入)

  • 特点
    • 乐观读锁:通过 tryOptimisticRead() 获取乐观锁(不加锁),读取后通过 validate() 校验数据是否被修改。
    • 性能更高:在读多写少的场景下,乐观读减少了锁的开销。
    • 缺点:使用更复杂,需处理数据一致性校验。
  • 适用场景
    • 高并发读取且数据一致性要求较低的场景(如缓存、日志记录)。
    • 需要极致性能优化的场景(如低延迟系统)。

3. 读写锁的核心方法

ReentrantReadWriteLock 为例:

ReadWriteLock lock = new ReentrantReadWriteLock();
// 读锁
lock.readLock().lock(); 
try {
    // 读取操作
} finally {
    lock.readLock().unlock();
}

// 写锁
lock.writeLock().lock(); 
try {
    // 写入操作
} finally {
    lock.writeLock().unlock();
}

4. 读写锁与互斥锁的对比

特性读写锁互斥锁(如 ReentrantLock
读操作并发性多个线程可同时读读操作需独占锁
写操作并发性写操作独占写操作独占
性能(读多写少)更高较低(因读操作频繁阻塞)
适用场景缓存、配置中心等读多写少场景通用场景(尤其是写多读少)

5. 读写锁的适用场景

  • 缓存系统:读取缓存频率远高于写入,适合用 ReentrantReadWriteLockStampedLock
  • 金融交易系统:强一致性要求,可能更适合 ReentrantLocksynchronized
  • 配置中心:读操作占绝对主导,适合 StampedLock 的乐观读模式。
  • 计数器更新:写操作频繁,推荐使用 ReentrantLock(减少锁状态切换开销)。

6. 读写锁的注意事项

  • 饥饿问题
    • 如果读线程持续占用读锁,写线程可能长时间无法获取锁(饥饿)。
    • 解决方案:使用公平锁ReentrantReadWriteLock(true)),按线程请求顺序分配锁。
  • 锁降级
    • 写锁降级为读锁时,必须先获取写锁,再获取读锁,最后释放写锁。
    • 示例:
      lock.writeLock().lock();
      try {
      // 写操作
      lock.readLock().lock(); // 降级为读锁
      // 释放写锁(保留读锁)
      lock.writeLock().unlock();
      } finally {
      lock.readLock().unlock();
      }
  • StampedLock 的复杂性
    • 乐观读需配合 validate() 校验数据一致性。
    • 若校验失败,需升级为悲观读锁或写锁。

7. 代码示例

ReentrantReadWriteLock 示例

public class Cache {
    private final ReadWriteLock lock = new ReentrantReadWriteLock();
    private Map<String, Object> data = new HashMap<>();

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

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

StampedLock 示例

public class OptimisticCache {
    private final StampedLock lock = new StampedLock();
    private Map<String, Object> data = new HashMap<>();

    public Object get(String key) {
        long stamp = lock.tryOptimisticRead(); // 乐观读
        Object value = data.get(key);
        if (lock.validate(stamp)) { // 校验数据一致性
            return value;
        }
        // 校验失败,升级为悲观读锁
        stamp = lock.readLock();
        try {
            return data.get(key);
        } finally {
            lock.unlockRead(stamp);
        }
    }

    public void put(String key, Object value) {
        long stamp = lock.writeLock();
        try {
            data.put(key, value);
        } finally {
            lock.unlockWrite(stamp);
        }
    }
}

8. 总结

  • 读写锁的核心价值:在读多写少的场景下,通过允许多线程并发读取提升性能。
  • 选择建议
    • 读多写少:优先使用 ReentrantReadWriteLockStampedLock
    • 强一致性:使用 ReentrantLocksynchronized
    • 极致性能:尝试 StampedLock 的乐观读模式,但需处理校验逻辑。
  • 关键点:避免锁饥饿、合理使用公平锁,并根据业务需求选择锁类型。

通过合理使用读写锁,可以在并发编程中平衡性能与安全性,尤其适合高并发的读操作场景。

THE END
喜欢就支持一下吧
点赞9 分享