缓存场景适合读写锁,因其支持读并发、读阻塞写、写独占,契合读多写少特性;而普通互斥锁使所有读操作串行化,浪费并发能力。

Java 中用读写锁实现缓存的线程安全读写,核心是让“读不阻塞读、读阻塞写、写阻塞一切”,特别适合配置、热点数据等读多写少的场景。关键不在加锁本身,而在锁粒度与操作边界的合理划分。
为什么缓存场景适合读写锁
普通互斥锁(如 synchronized 或 ReentrantLock)会让所有读请求排队等待,哪怕只是取一个不变的配置值——这白白浪费并发能力。而缓存数据天然具备“读操作不改变状态”的特性,多个线程同时读完全安全。读写锁正是利用这一点:允许多个线程并发读,只在真正修改(如更新缓存条目、刷新过期数据)时才独占写锁。
- 读多写少是前提:比如用户权限配置、系统开关、静态字典表
- 写操作频率低但需强一致性:例如后台定时刷新缓存或主动失效某 key
- 避免“缓存击穿”类问题:多个线程同时发现缓存 miss,争抢去数据库加载,造成重复查询——用写锁保护加载过程可解决
基础结构:用 ReentrantReadWriteLock 包裹 Map
不要直接对整个 HashMap 加 synchronized,而是用 ReentrantReadWriteLock 分离读写路径。注意:锁对象必须是实例变量,且被所有读写方法共用。
- 声明一把读写锁:
private final ReadWriteLock rwLock = new ReentrantReadWriteLock(); - 读操作用
rwLock.readLock(),写操作用rwLock.writeLock() - 必须配合 try-finally 确保锁释放,否则极易死锁或资源泄漏
典型读写方法写法(以 get/put 为例)
读方法只持读锁,写方法只持写锁,两者互不影响;但写锁获取时会等待所有读锁释放,读锁获取时若已有写锁则等待。
立即学习“Java免费学习笔记(深入)”;
-
读操作(get):加读锁 → 安全读取 → 立即释放
public Object get(String key) {<br> rwLock.readLock().lock();<br> try { return cacheMap.get(key); }<br> finally { rwLock.readLock().unlock(); }<br>} -
写操作(put):加写锁 → 安全写入 → 立即释放
public void put(String key, Object value) {<br> rwLock.writeLock().lock();<br> try { cacheMap.put(key, value); }<br> finally { rwLock.writeLock().unlock(); }<br>} -
带加载逻辑的 get(防击穿):先尝试读,miss 后升级为写锁加载,再放回缓存
注意:不能在持有读锁时直接 try 获取写锁(会死锁),必须先释放读锁再获取写锁
容易踩的坑和应对建议
读写锁不是银弹,用错反而比普通锁更慢。
- 锁粒度过粗:比如整个缓存共用一把读写锁,而实际业务中不同 key 之间无依赖,可考虑分段锁或 ConcurrentHashMap + CAS 替代
- 读锁未及时释放:尤其在读操作里调用外部服务或 sleep,会导致其他写线程长时间等待——读逻辑应尽量轻量
- 误以为能“读锁升级”:JDK 明确禁止在持有读锁时获取写锁,必须显式释放读锁后再申请写锁
-
忽略公平性选择:默认非公平策略吞吐高但可能饿死写线程;高写频场景可启用公平模式:
new ReentrantReadWriteLock(true)


















