中 进阶
读写锁原理#
一句话答案#
ReadWriteLock 读共享写独占,state 高 16 位读锁低 16 位写锁,支持锁降级(写→读)不支持升级(会死锁)。
核心要点
ReentrantReadWriteLock 核心设计:
- state 拆分:int 32位 = 高16位(读锁持有数)+ 低16位(写锁重入数)
- 读锁:共享模式,多个线程可同时持有
- 写锁:独占模式,有写锁时其他读/写都阻塞
获取规则:
| 当前状态 | 获取读锁 | 获取写锁 |
|---|---|---|
| 无锁 | ✅ | ✅ |
| 有读锁 | ✅ | ❌(等待) |
| 有写锁(非自己) | ❌(等待) | ❌(等待) |
| 有写锁(自己) | ✅(锁降级) | ✅(重入) |
锁降级:写锁 → 获取读锁 → 释放写锁(安全降级,保证数据可见性) 锁升级:不支持!(读锁→写锁会死锁)
适用场景: 缓存、配置管理(读多写少) 缺陷: 写线程可能饥饿 → JDK8 引入 StampedLock(乐观读)
面试回答(2分钟版)
ReentrantReadWriteLock 实现了读共享写独占的锁机制,适用于读多写少场景。它基于 AQS,巧妙地将 state 的 32 位拆成高 16 位记录读锁持有数、低 16 位记录写锁重入数。多个线程可以同时获取读锁并发读取,但有线程持有写锁时其他线程的读写请求都会阻塞,保证写操作的独占性。一个重要特性是支持锁降级:持有写锁的线程可以再获取读锁,然后释放写锁,安全地从写模式切换到读模式,保证数据修改后立即对当前线程可见。但不支持锁升级,如果读锁线程尝试获取写锁会造成死锁,因为写锁要等所有读锁释放,而自己持有读锁不会释放。读写锁的一个缺陷是写线程可能饥饿:大量读线程持续进入导致写线程长期等不到锁。JDK 8 引入了 StampedLock 来解决这个问题,它提供乐观读模式,读操作不加锁而是获取一个 stamp 版本号,读完后验证版本号是否被写操作改变,没改变就直接用,改变了再退化为悲观读锁。
追问与易错
追问方向:
- “读写锁饥饿怎么解决?”→ 使用公平模式 new ReentrantReadWriteLock(true) 让写线程按顺序获锁;或升级到 JDK8 的 StampedLock,其乐观读模式不阻塞写线程
- “锁降级的意义?”→ 持有写锁时先获取读锁再释放写锁,保证当前线程能立即看到自己的修改结果,同时允许其他线程并发读;如果直接释放写锁再获取读锁,中间可能被其他写线程修改数据
- “StampedLock 的区别?”→ StampedLock 提供乐观读模式,读操作不加锁只获取 stamp 版本号,读完后验证版本是否变化,没变则直接用,变了退化为悲观读锁;性能优于 ReadWriteLock 但不支持重入和 Condition
易错点:
- ❌ 读锁可以升级为写锁——不支持会死锁!
- ❌ 读写锁一定比 synchronized 快——读少写多时反而慢