ReentrantReadWriteLock 不支持锁升级,持读锁时调用 writeLock.lock() 会因校验失败而永久 WAITING;正确做法是用 writeLock.tryLock() 抢占或 readLock.tryLock() 非阻塞获取后降级,避免死锁。

ReentrantReadWriteLock 不支持锁升级,任何试图在持有读锁时获取写锁的操作都会导致线程永久 WAITING(死锁表象),这不是 bug,是设计使然。
为什么 lock() 读锁后调用 writeLock.lock() 会卡住
典型错误场景:缓存未命中时先用 readLock.lock() 查一次,发现没数据,接着想「顺手」加写锁更新——这直接触发锁升级,而 JDK 明确禁止。
- 线程持读锁时,
writeLock.lock()会检查是否有其他写锁占用;它发现当前无写锁,但紧接着校验「当前线程是否已持读锁」→ 是 → 拒绝获取写锁,并进入 AQS 队列等待 - 可问题在于:读锁没释放,写锁又拿不到,其他线程也进不来(读写互斥),自己又不放读锁 → 全局阻塞
- 线程 dump 中你会看到类似
WAITING (parking) at ... ReentrantReadWriteLock$ReadLock.lock,但实际卡在写锁获取环节
锁降级看似安全,但 tryLock() 是唯一可行路径
真正能落地的锁降级,必须绕开 readLock.lock() 的阻塞逻辑——因为你在写锁未释放时再去调 lock(),极易因读锁计数已达上限(如并发读线程太多)而把自己挂起,形成自锁。
- 正确姿势是:先用
readLock.tryLock()尝试非阻塞获取读锁;失败就退避或重试,绝不硬等 - 成功后再立即
writeLock.unlock(),此时读锁已稳稳持有,数据可见性连续 - 如果用
readLock.lock()替代tryLock(),哪怕只有一处调用,在高并发读场景下就可能让当前线程陷入无限 park
缓存加载中「读-判断-写-再读」流程的典型陷阱
这是最容易诱发死锁的业务模式:多个线程同时查缓存,都发现 miss,然后各自尝试写入。若没有统一的「写入协调机制」,仅靠读写锁无法避免竞争和误判。
- 单纯用
readLock+if (cache.get() == null)+writeLock.lock()是错的:两次读之间存在竞态窗口,多个线程可能同时进入写分支 - 若在写锁内再做一次
cache.get()双检,能防重复写,但无法解决「先读锁后升写锁」这个根本死锁点 - 真正健壮的做法是:一开始就用
writeLock.tryLock()尝试抢占;失败则让出,或 fallback 到阻塞等待,但绝不在读锁上下文中发起写锁请求
最易被忽略的一点:锁降级不是为了「省一次锁开销」,而是为了保证「写后立刻读」的数据一致性。一旦你把降级当成通用流程嵌套进复杂条件分支,尤其是混入异常处理、超时控制或回调逻辑,tryLock() 的失败路径就极难覆盖周全——这时候不如老实用写锁兜底,逻辑反而清晰可控。










