ReentrantReadWriteLock详解
ReentrantReadWriteLock 是 Java 提供的可重入读写锁。它适合“读多写少”的场景,例如配置缓存、商品详情缓存、字典数据缓存。
如果要从零理解它的完整原理、AQS 状态拆分、读锁写锁获取流程、锁降级、线上排查和面试回答,建议继续看:读写锁全过程原理。
零基础可以先这样理解:
读锁允许多个线程一起读;写锁要求独占,写的时候不能有人读,也不能有人写。
如果只用普通互斥锁,多个读操作也会互相阻塞,读多写少时性能会浪费。读写锁把访问分成读和写两类,让安全的并发读可以同时进行。
为什么读写锁只适合读多写少
读写锁的收益来自“多个读线程可以同时进入”。如果写操作很频繁,读锁和写锁会频繁互斥,维护读写状态、排队和唤醒的成本反而可能比普通互斥锁更高。
flowchart TD
A["读多写少"] --> B["多个读线程共享读锁"]
B --> C["写线程偶尔独占"]
C --> D["整体吞吐提升"]
E["写多或读写都多"] --> F["读写频繁互相阻塞"]
F --> G["读写锁收益下降"]工作原理图
flowchart TD
A["线程请求锁"] --> B{"请求读锁还是写锁"}
B -- "读锁" --> C{"当前是否有写锁持有"}
C -- "没有" --> D["读计数 + 1\n允许多个读线程同时进入"]
C -- "有" --> E["进入 AQS 队列等待"]
B -- "写锁" --> F{"当前是否无人读且无人写"}
F -- "是" --> G["写计数 + 1\n写线程独占进入"]
F -- "否" --> E
D --> H["读完释放读锁\n读计数 - 1"]
G --> I["写完释放写锁\n写计数 - 1"]
H --> J{"是否可以唤醒队列线程"}
I --> J
J -- "可以" --> K["唤醒后继线程继续竞争"]AQS 的 state 是一个 int,读写锁把它拆成两半使用:高 16 位记录读锁次数,低 16 位记录写锁重入次数。因此它能同时管理“有多少读线程”和“写线程重入了几次”。
类结构分析
ReentrantReadWriteLock是ReadWriteLock(读写锁)的实现类。ReadWriteLock接口如下:
public interface ReadWriteLock {
/**
* Returns the lock used for reading.
*
* @return the lock used for reading
*/
Lock readLock();
/**
* Returns the lock used for writing.
*
* @return the lock used for writing
*/
Lock writeLock();
}构造方法
ReentrantReadWriteLock的无参构造默认创建非公平的同步机制。方法如下:
public ReentrantReadWriteLock() {
this(false);
}
/**
* Creates a new {@code ReentrantReadWriteLock} with
* the given fairness policy.
*
* @param fair {@code true} if this lock should use a fair ordering policy
*/
public ReentrantReadWriteLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
readerLock = new ReadLock(this);
writerLock = new WriteLock(this);
}其中FairSync和NonfairSync都是Sync的子类,一个是公平的一个是非公平的。
/**
* Nonfair version of Sync
*/
static final class NonfairSync extends Sync {
private static final long serialVersionUID = -8159625535654395037L;
final boolean writerShouldBlock() {
return false; // writers can always barge
}
final boolean readerShouldBlock() {
return apparentlyFirstQueuedIsExclusive();
}
}
/**
* Fair version of Sync
*/
static final class FairSync extends Sync {
private static final long serialVersionUID = -2274990926593161451L;
final boolean writerShouldBlock() {
return hasQueuedPredecessors();
}
final boolean readerShouldBlock() {
return hasQueuedPredecessors();
}
}和ReentrantLock一样,有一个Sync的静态内部类也是继承了AbstractQueuedSynchronizer但实现不同。另外还有ReadLock和WriteLock这两个比较重要的内部类。 ReadLock和WriteLock都实现了Lock接口
使用 Demo:读多写少缓存
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class ConfigCache {
private final Map<String, String> cache = new HashMap<>();
private final ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
public String get(String key) {
rwLock.readLock().lock();
try {
return cache.get(key);
} finally {
rwLock.readLock().unlock();
}
}
public void put(String key, String value) {
rwLock.writeLock().lock();
try {
cache.put(key, value);
} finally {
rwLock.writeLock().unlock();
}
}
}这个例子里,多个线程同时 get() 不会互相阻塞;只要有线程 put(),写锁会独占,避免读到写入一半的数据。
读锁和写锁的关系
| 场景 | 是否允许 |
|---|---|
| 多个线程同时读 | 允许 |
| 一个线程写,其他线程读 | 不允许 |
| 一个线程写,其他线程写 | 不允许 |
| 持有写锁的线程再获取写锁 | 允许,可重入 |
| 持有写锁的线程再获取读锁 | 允许,叫锁降级 |
| 持有读锁后升级成写锁 | 不建议,容易死锁 |
锁降级示例:
rwLock.writeLock().lock();
try {
// 修改缓存
cache.put("mode", "new");
// 写锁未释放前先拿读锁,保证后续读取的是自己刚写入的数据。
rwLock.readLock().lock();
} finally {
rwLock.writeLock().unlock();
}
try {
System.out.println(cache.get("mode"));
} finally {
rwLock.readLock().unlock();
}使用读写锁时要先确认业务确实是读多写少。如果写操作很频繁,读线程会经常被写锁阻塞,收益就不明显。
常见风险
| 风险 | 后果 | 正确做法 |
|---|---|---|
| 持有读锁时直接申请写锁 | 可能死锁,读锁不释放写锁拿不到 | 不做锁升级,先释放读锁再竞争写锁 |
| 读操作里修改共享对象 | 多个读线程同时修改导致数据错乱 | 读锁内只读不可变或稳定状态 |
| 写锁持有时间过长 | 所有读线程被阻塞 | 写锁内只做必要修改 |
| 忘记 finally 解锁 | 锁永远不释放,线程全部卡住 | lock() 后必须 try/finally unlock() |
