本文目录导读:

- 目录导读
- 读写锁的核心概念与适用场景
- Java内置读写锁:ReentrantReadWriteLock(含完整案例)
- 手写一个简易读写锁(自定义实现)
- 读写锁常见陷阱与性能优化
- Q&A:高频面试题与实战解答
- 总结:何时用读写锁,何时不用
Java案例详解:如何实现高性能读写锁(ReadWriteLock)实战指南
目录导读
- 读写锁的核心概念与适用场景
- Java内置读写锁:ReentrantReadWriteLock(含案例)
- 手写一个简易读写锁(自定义实现)
- 读写锁常见陷阱与性能优化
- Q&A:高频面试题与实战解答
- 何时用读写锁,何时不用
读写锁的核心概念与适用场景
在高并发编程中,锁是保证数据一致性的关键工具,但传统的 synchronized 或 ReentrantLock 是“互斥锁”——所有线程(读或写)都只能串行执行,在实际业务中,读操作远多于写操作(比如缓存、配置中心、读多写少的数据库连接池),如果读线程之间也互斥,会严重浪费性能。
读写锁(ReadWriteLock) 正是为了解决这个问题而设计的:
- 读锁(共享锁):多个线程可以同时持有读锁(只要没有写锁),适用于只读操作。
- 写锁(排他锁):当一个线程持有写锁时,其他所有线程(无论读还是写)都必须等待,适用于修改操作。
适用场景:
- 缓存系统(如 Redis 本地缓存同步)
- 配置热更新(读配置频繁,修改配置极少)
- 共享数据的批量读取 vs 偶发写入(如排行榜、商品库存缓存)
Java内置读写锁:ReentrantReadWriteLock(含完整案例)
Java 在 java.util.concurrent.locks 中提供了 ReentrantReadWriteLock,它是可重入的(同一个线程可以多次获取读锁或写锁),支持公平/非公平策略。
案例:线程安全的本地缓存实现
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class ReadWriteCache {
private final Map<String, Object> cache = new HashMap<>();
private final ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
// 写数据(写锁)
public void put(String key, Object value) {
rwLock.writeLock().lock();
try {
System.out.println(Thread.currentThread().getName() + " 正在写入数据...");
// 模拟耗时写操作
Thread.sleep(100);
cache.put(key, value);
System.out.println(Thread.currentThread().getName() + " 写入完成");
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
rwLock.writeLock().unlock();
}
}
// 读数据(读锁)
public Object get(String key) {
rwLock.readLock().lock();
try {
System.out.println(Thread.currentThread().getName() + " 正在读取数据...");
// 模拟耗时读操作
Thread.sleep(50);
return cache.get(key);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return null;
} finally {
rwLock.readLock().unlock();
}
}
// 测试并发读写
public static void main(String[] args) {
ReadWriteCache myCache = new ReadWriteCache();
// 启动一个写线程
new Thread(() -> myCache.put("key1", "value1"), "写线程-1").start();
// 启动多个读线程
for (int i = 0; i < 3; i++) {
new Thread(() -> {
System.out.println("读取结果: " + myCache.get("key1"));
}, "读线程-" + i).start();
}
}
}
运行效果解析:
- 写线程获取写锁时,所有读线程阻塞等待。
- 当写锁释放后,多个读线程几乎同时获取读锁,并行读取,不再串行。
- 这显著降低了读操作的响应时间,提高了吞吐量。
注意事项:
- 不要忘记在
finally中释放锁,避免死锁。 - 写锁可以降级为读锁(通过先获取写锁,再获取读锁,最后释放写锁),但读锁不能升级为写锁(否则死锁)。
手写一个简易读写锁(自定义实现)
为了加深理解,我们基于 synchronized + 等待/通知 机制实现一个简单的读写锁(非重入版本)。
public class SimpleReadWriteLock {
private int readers = 0; // 当前持有读锁的线程数
private int writers = 0; // 当前持有写锁的线程数(只能0或1)
private int writeRequests = 0; // 等待写锁的线程数
// 获取读锁
public synchronized void lockRead() throws InterruptedException {
while (writers > 0 || writeRequests > 0) {
wait(); // 有写线程在活动或等待时,阻塞
}
readers++;
}
// 释放读锁
public synchronized void unlockRead() {
readers--;
notifyAll();
}
// 获取写锁
public synchronized void lockWrite() throws InterruptedException {
writeRequests++;
while (readers > 0 || writers > 0) {
wait(); // 等待所有读锁和写锁释放
}
writeRequests--;
writers = 1;
}
// 释放写锁
public synchronized void unlockWrite() {
writers = 0;
notifyAll();
}
}
缺陷:
- 不支持可重入(同线程不能重复加锁)
- 不公平(可能有写线程一直饥饿)
- 性能不如
ReentrantReadWriteLock(使用了全局synchronized)
价值:展示了读写锁最本质的“读共享、写互斥”逻辑,适合面试手写。
读写锁常见陷阱与性能优化
陷阱1:读锁阻塞写锁(写线程饥饿)
如果一直有读线程获取读锁,写线程可能永远拿不到写锁。解决方案:
- 使用
ReentrantReadWriteLock的公平模式(new ReentrantReadWriteLock(true)) - 或自定义策略:当写线程等待时,禁止新读线程进入(如上面的自定义实现中的
writeRequests判断)
陷阱2:锁降级导致死锁
错误示例:
writeLock.lock(); readLock.lock(); // 先获取读锁 writeLock.unlock(); // 再释放写锁 // 此时仍然占用读锁(降级成功)
但反过来(读锁升级为写锁)会导致死锁,因为一个线程持有读锁时,其他线程无法获取写锁,而它自己又在等待写锁。
性能优化建议
- 如果读操作非常快(微秒级),读写锁的开销可能比直接用
synchronized还高(锁获取/释放的CAS操作),可以用LongAdder或无锁结构代替。 - 对于“写极少,读极多”的场景,可以考虑
CopyOnWriteArrayList或StampedLock(乐观读锁,JDK8+)。
Q&A:高频面试题与实战解答
Q1:读写锁和互斥锁的性能差距有多大?
A:在读多写少的场景下(例如90%读,10%写),读写锁的吞吐量可能是互斥锁的3-10倍,但具体取决于并发线程数和锁的竞争程度。
Q2:ReentrantReadWriteLock 的读锁可以重入吗?
A:可以,同一线程可以多次获取读锁(需要释放相同次数),但写锁重入时,如果已经持有读锁,会死锁(不支持升级)。
Q3:如果业务要求“读不允许等待写”,怎么办?
A:可以使用 StampedLock 的乐观读模式(tryOptimisticRead),写操作不阻塞读,但读操作需要验证数据版本。
Q4:手写读写锁时,如何避免线程饥饿?
A:可以在读锁获取前检查是否有写线程在等待(如 writeRequests > 0 则阻塞),或者引入公平队列(如使用 AbstractQueuedSynchronizer 的子类)。
何时用读写锁,何时不用
| 场景 | 推荐方案 |
|---|---|
| 读远多于写(如缓存、配置) | 使用 ReentrantReadWriteLock 或 StampedLock |
| 读写频率相近 | 普通互斥锁即可(如 ReentrantLock) |
| 读操作极快(微秒级) | 优先考虑无锁方案(如 volatile、AtomicReference) |
| 需要乐观读且容忍短暂不一致 | StampedLock 乐观读 |
核心原则:不要因为 Java 提供了读写锁就盲目使用,先评估业务场景的读写比例、锁持有时间、并发量,再做出选择,如果读锁保护的关键路径很短(如几十微秒),互斥锁的简单性往往更好。
通过本文的案例代码和原理分析,你应该能熟练在 Java 项目中实现读写锁,并能应对面试中的手写锁问题,如果还有疑问,欢迎在评论区交流。