
锁降级在 ReentrantReadWriteLock 中根本不能直接做
ReentrantReadWriteLock 不支持“写锁降级为读锁”以外的任何降级操作,而且这个“降级”也不是原子动作——它只是你手动释放写锁、再获取读锁的过程。很多人以为调用某个函数就能自动完成,其实没有 downgrade() 这种方法,更不存在 tryConvertToReadLock()。
常见错误现象是:线程持有写锁后,试图不释放就调用 readLock().lock(),结果被自己阻塞(因为写锁还没放,读锁无法重入);或者误以为 writeLock().unlock() 后读锁会“继承”写锁的持有权,导致数据被其他线程修改。
- 必须先
writeLock().unlock(),再readLock().lock() - 中间不能有非原子操作(比如网络请求、日志打印),否则降级窗口期可能被其他写线程抢占
- 如果多个线程都走这套流程,得靠外部同步或额外状态位控制,否则可能读到过期值
为什么非要“先解锁再加读锁”而不是设计成原子操作
这是 ReentrantReadWriteLock 的设计取舍:它把锁语义和线程调度完全交给 JVM,不引入跨锁状态机。写锁和读锁底层共享同一个 AQS state 字段,但 bit 位划分固定(高16位读计数,低16位写重入计数),没法在不破坏 state 含义的前提下“切换类型”。
性能上,强制原子降级需要在 AQS 中增加额外 CAS 循环和状态校验,反而拖慢高频写场景;兼容性上,老版本 JDK(如 6/7)的 AQS 实现也不支持这种扩展。
立即学习“Java免费学习笔记(深入)”;
- 读写锁共用
Sync内部类,state 没有预留“过渡态”空间 -
tryAcquireShared()和tryAcquire()是两个独立入口,无共享上下文 - JDK 官方文档明确说:“Lock downgrading is supported... by acquiring the read lock before releasing the write lock”——注意是“before releasing”,不是“while holding”
正确实现锁降级的三步写法(带防御)
典型场景:更新缓存后立刻读取,希望避免写锁释放后被其他线程覆盖。关键不是“怎么写”,而是“怎么防断层”。
示例中 cache 是 volatile 引用,data 是实际内容,降级前需确保引用已更新且可见:
// 假设 data 已计算完毕
writeLock.lock();
try {
cache = data; // volatile 写,保证可见性
} finally {
writeLock.unlock(); // 必须在这里释放
}
// ↓ 降级窗口开始:此时 cache 可能已被其他线程改写
readLock.lock();
try {
return cache; // 读取最新值(可能是别人刚写的)
} finally {
readLock.unlock();
}
- volatile 写必须在
writeLock.unlock()前完成,否则读线程看不到更新 - 不能把
readLock.lock()放进 writeLock 的 try 块里——编译器可能重排序,JVM 不保证锁释放顺序 - 如果业务要求“必须读到自己刚写的值”,就得放弃降级,改用纯写锁 + 多次读,或引入版本号校验
容易被忽略的线程安全破口
最常翻车的地方不是锁本身,而是降级后对共享对象的访问方式。ReentrantReadWriteLock 只管锁的获取与释放,不管对象内部是否线程安全。
比如你降级后拿到一个 ConcurrentHashMap 引用,以为“读锁保护了整个 map”,其实没用——读锁只保护你获取引用的动作,map 里面的 put()、computeIfAbsent() 还是各自加自己的锁。
- 降级后调用
cache.get(key)是安全的,但cache.put(key, v)会触发新的写操作,必须重新申请写锁 - 如果
cache是ArrayList,即使持有读锁,调用get(0)也可能抛ConcurrentModificationException(除非你确认没人并发修改结构) - 锁降级不传递 happens-before 关系到对象字段,所以字段级 volatile 或 final 修饰不能省
事情说清了就结束










