ReentrantReadWriteLock不支持锁降级:不允许先持有写锁再直接获取读锁,否则会导致死锁;正确做法是先显式获取读锁,再释放写锁,且必须无间隙、同线程完成。

Java中ReentrantReadWriteLock不支持锁降级
直接说结论:ReentrantReadWriteLock不允许“先持有写锁,再获取读锁”——也就是所谓“锁降级”在API层面被明确禁止。调用readLock().lock()时如果当前线程已持有写锁,会直接死锁(或无限阻塞,取决于是否使用tryLock())。
这不是bug,是设计使然。JDK文档明确写道:“A write lock can be downgraded to a read lock by acquiring the read lock and then releasing the write lock”,但注意——它要求你**先显式获取读锁,再释放写锁**,中间不能有间隙,且必须由同一个线程完成。而现实中,很多人误以为可以“在写锁未释放时直接调readLock().lock()”,这行不通。
- 常见错误现象:
Thread A持writeLock,接着调readLock().lock()→ 线程卡住,CPU空转,jstack可见WAITING onAbstractQueuedSynchronizer$ConditionObject - 根本原因:锁状态是单个int字段的高位(读锁计数)+低位(写锁重入数),写锁独占时,读锁获取逻辑会检查“当前无其他写锁持有者”,但忽略自己——可JDK偏偏又加了线程身份校验,禁止同一线程重复进入读锁段(避免计数错乱)
- 兼容性影响:所有JDK版本(5–21)行为一致,别指望升级解决
真·锁降级的唯一安全写法:先抢读锁,再放写锁
想实现“写完立刻以读视角继续访问,且保证中间不被其他写线程插队”,只能靠顺序严谨的三步:获取读锁 → 释放写锁 → 继续读操作。关键在于,这两把锁的交接必须原子、无间隙。
示例代码核心逻辑:
立即学习“Java免费学习笔记(深入)”;
// 假设已持有 writeLock
// Step 1:尝试非阻塞获取读锁(必须用 tryLock,否则可能死锁)
if (!readLock.tryLock()) {
// 失败说明有其他写线程正在竞争,需退避或重试
writeLock.unlock();
throw new IllegalStateException("Failed to acquire read lock for downgrade");
}
// Step 2:立即释放写锁(此时读锁已稳稳持有)
writeLock.unlock();
// Step 3:安全读取(其他线程可并发读,但无法写)
return cachedData;
- 为什么必须用
tryLock()?因为lock()会阻塞,而你已持写锁,一旦读锁被其他线程占满(比如12个读线程),你自己也会被挂起——形成自锁 - 性能代价:多一次CAS争抢(读锁获取),但换来的是数据一致性保障;相比全量加写锁跑完再放开,整体吞吐通常更高
- 容易踩的坑:忘记在
tryLock()失败后释放写锁,导致锁泄漏;或者没做异常兜底,让写锁永远不释放
替代方案:用StampedLock更简洁地处理读写切换
如果你真正想要的是“写完立刻转为乐观读/悲观读,且不希望手动管理两把锁”,StampedLock才是更现代的选择。它的tryConvertToReadLock(long)能直接把写戳记(stamp)转成读戳记,失败则返回0,不阻塞。
典型用法:
long stamp = sl.writeLock();
try {
// 修改数据
data = newData;
// 尝试降级:把写戳记转为读戳记
long readStamp = sl.tryConvertToReadLock(stamp);
if (readStamp != 0) {
stamp = readStamp; // 成功,继续用这个stamp读
} else {
// 失败,说明有其他写线程介入,需重新获取读锁
stamp = sl.readLock();
}
return data; // 安全返回
} finally {
sl.unlock(stamp); // 一把unlock收尾
}
- 优势:
StampedLock无锁饥饿问题,支持乐观读(validate()),适合读多写少场景 - 注意点:它不支持重入,也不与
Condition配合;tryConvertToReadLock仅在无其他写线程竞争时成功,不是100%可靠,需配合失败回退 - 兼容性:JDK 8+,低版本不可用
为什么“保持写锁同时读”本质上危险?
这个问题背后常藏着一个隐含假设:“我写完了,数据已一致,读一下没问题”。但JVM指令重排、CPU缓存可见性、GC safepoint插入,都可能让“写完”和“读到最新值”之间出现裂缝。真正的安全边界不是“写操作结束”,而是“锁释放并重新获取”的内存屏障时机。
- 最容易被忽略的一点:即使你绕过
ReentrantReadWriteLock限制(比如用两个独立ReentrantLock模拟),也无法获得读写锁组合的语义保障——比如读锁的共享性、写锁的排他性、以及两者之间的互斥规则 - 另一个盲区:很多业务以为“只读自己刚写的对象”就不用锁,但若该对象被其他线程作为参数传递、放入全局容器、或触发监听器回调,就会暴露竞态
- 复杂点始终在边界上:锁降级不是语法糖,它是对“修改-发布-消费”这一整条内存可见性链路的精确控制,少一步,就可能在高并发下偶现脏读










