ReadWriteLock 读写锁
版本基线:本文按 JDK 20 的
java.util.concurrent.locksAPI 编写;示例优先使用可直接编译运行的标准库能力。
学习目标
- 会用
ReentrantReadWriteLock的读锁和写锁分别保护共享状态。 - 能正确写出写锁降级流程,并解释为什么读锁不能直接升级为写锁。
- 能根据读写比例、临界区长度和公平性要求判断读写锁是否值得使用。
核心知识点
专业术语
- ReadWriteLock:把一个共享资源拆成读锁和写锁的锁协议;多个读者可以并行,写者必须独占。
- ReentrantReadWriteLock:JDK 提供的可重入读写锁实现,支持公平/非公平策略以及读写锁的中断、超时获取。
- 读锁(read lock):共享锁;持有读锁的线程之间可以并行,但会阻塞写锁。
- 写锁(write lock):独占锁;获得写锁后不能再有其他读者或写者进入。
- 锁降级(lock downgrading):先持有写锁,再获取读锁,最后释放写锁,让线程继续以读者身份观察状态。
- 锁升级(lock upgrading):持有读锁时再申请写锁;
ReentrantReadWriteLock不提供安全的直接升级路径。
白话解释与边界
把锁想成一间阅览室:读者可以一起看,写作者进来时必须清场。只有在“读操作明显多、读临界区不太短、写操作相对少”的情况下,并发读才可能抵消读写锁本身的管理成本。若读操作只是读取一个字段,普通 synchronized、ReentrantLock 或不可变快照往往更清晰。
读写锁只保护遵守同一锁协议的访问路径。任何绕过读锁/写锁的直接字段访问,都会破坏这个协议;它也不会自动让多个业务步骤组成事务。锁降级可以保证写入完成后继续读取同一份状态,锁升级则可能因为自己持有读锁而把写者永远挡住。
常用用法
ReentrantReadWriteLock():创建非公平读写锁
默认构造器适合先验证读多写少的并发模型;非公平策略通常吞吐更高,但不保证等待顺序。
// 说明:ReentrantReadWriteLock():创建非公平读写锁。
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class ReadWriteLockCreateDemo {
public static void main(String[] args) {
var lock = new ReentrantReadWriteLock();
// 初始状态:lock = new ReentrantReadWriteLock()。
// 作用:var lock = new ReentrantReadWriteLock();;创建非公平读写锁。
System.out.println("fair=" + lock.isFair());
// 输出:fair=false
}
}默认构造器使用非公平策略,通常吞吐更高,但不承诺严格的先来先得顺序。先用默认策略写出正确的锁协议,再根据等待时间和饥饿证据评估公平锁。
readLock():用读锁保护只读临界区
只读临界区需要允许多个线程并行访问时使用读锁;读取必须短小,不能把未知 I/O 长时间放在锁内。
// 说明:readLock():用读锁保护只读临界区。
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class ReadLockDemo {
private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
// 初始状态:lock = new ReentrantReadWriteLock()。
// 作用:private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();;用读锁保护只读临界区。
private int value = 42;
// 初始状态:value = 42。
int read() {
lock.readLock().lock();
// 初始状态:lock.readLock().lock()。
// 作用:lock.readLock().lock();;用读锁保护只读临界区。
try {
return value;
} finally {
lock.readLock().unlock();
// 初始状态:lock.readLock().unlock()。
// 作用:lock.readLock().unlock();;用读锁保护只读临界区。
}
}
public static void main(String[] args) {
var state = new ReadLockDemo();
// 初始状态:state = new ReadLockDemo()。
// 作用:var state = new ReadLockDemo();;用读锁保护只读临界区。
System.out.println(state.read());
// 输出:42
}
}读锁允许多个线程同时读取,但读操作也必须保持短小;如果读取期间还要调用未知代码或执行 I/O,读锁可能长时间阻塞写者。
writeLock():用写锁保护独占更新
更新共享状态或维护多个字段不变式时适合使用写锁;成功获取后必须在 finally 中释放。
// 说明:writeLock():用写锁保护独占更新。
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class WriteLockDemo {
private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
// 初始状态:lock = new ReentrantReadWriteLock()。
// 作用:private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();;用写锁保护独占更新。
private int value;
void add(int delta) {
lock.writeLock().lock();
// 初始状态:lock.writeLock().lock()。
// 作用:lock.writeLock().lock();;用写锁保护独占更新。
try {
value += delta;
} finally {
lock.writeLock().unlock();
// 初始状态:lock.writeLock().unlock()。
// 作用:lock.writeLock().unlock();;用写锁保护独占更新。
}
}
public static void main(String[] args) {
var state = new WriteLockDemo();
// 初始状态:state = new WriteLockDemo()。
// 作用:var state = new WriteLockDemo();;用写锁保护独占更新。
state.add(5);
// 初始状态:state.add(5)。
// 作用:state.add(5);;用写锁保护独占更新。
System.out.println("updated");
// 输出:updated
}
}写锁同时排斥读者和其他写者,适合维护多个字段之间的不变式。每次成功获得写锁都要在 finally 中释放,不能因为方法提前 return 或抛异常而跳过解锁。
锁降级:写锁转为读锁
写入后还要继续读取同一份状态时使用锁降级;先取得读锁再释放写锁,避免中间被其他写者插入。
// 说明:锁降级:写锁转为读锁。
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class LockDowngradeDemo {
private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
// 初始状态:lock = new ReentrantReadWriteLock()。
// 作用:private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();;写锁转为读锁。
private int version;
int refreshAndRead() {
lock.writeLock().lock();
// 初始状态:lock.writeLock().lock()。
// 作用:lock.writeLock().lock();;写锁转为读锁。
try {
version++;
lock.readLock().lock();
// 初始状态:lock.readLock().lock()。
// 作用:lock.readLock().lock();;写锁转为读锁。
} finally {
lock.writeLock().unlock();
// 初始状态:lock.writeLock().unlock()。
// 作用:lock.writeLock().unlock();;写锁转为读锁。
}
try {
return version;
} finally {
lock.readLock().unlock();
// 初始状态:lock.readLock().unlock()。
// 作用:lock.readLock().unlock();;写锁转为读锁。
}
}
public static void main(String[] args) {
var state = new LockDowngradeDemo();
// 初始状态:state = new LockDowngradeDemo()。
// 作用:var state = new LockDowngradeDemo();;写锁转为读锁。
System.out.println(state.refreshAndRead());
// 输出:1
}
}降级的顺序必须是“先获得读锁,再释放写锁”。这样释放写锁的瞬间,当前线程仍在读锁保护下;若先释放写锁再申请读锁,中间可能被其他写者插入,读到的就不一定是刚刚更新的状态。
ReentrantReadWriteLock(boolean fair):创建公平读写锁
等待顺序和饥饿风险需要更可控时选择公平构造器;公平策略会增加排队成本,不能替代超时和取消。
// 说明:ReentrantReadWriteLock(boolean fair):创建公平读写锁。
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class FairReadWriteLockDemo {
public static void main(String[] args) {
var lock = new ReentrantReadWriteLock(true);
// 初始状态:lock = new ReentrantReadWriteLock(true)。
// 作用:var lock = new ReentrantReadWriteLock(true);;创建公平读写锁。
System.out.println("fair=" + lock.isFair());
// 输出:fair=true
}
}公平模式倾向于让等待时间更长的线程先获得锁,降低插队机会,但会增加排队成本。公平策略不是严格实时调度,也不能替代合理的超时、取消和临界区设计。
不常用但需要知道
读锁升级:不要在读锁内直接申请写锁
读操作发现需要更新时不要直接升级;先释放读锁再重新竞争写锁,否则可能因其他读者而永久等待。
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class LockUpgradeRiskDemo {
public static void main(String[] args) {
var lock = new ReentrantReadWriteLock();
// 初始状态:lock = new ReentrantReadWriteLock()。
// 作用:var lock = new ReentrantReadWriteLock();;不要在读锁内直接申请写锁。
lock.readLock().lock();
// 初始状态:lock.readLock().lock()。
// 作用:lock.readLock().lock();;不要在读锁内直接申请写锁。
try {
boolean upgraded = lock.writeLock().tryLock();
// 初始状态:upgraded = lock.writeLock().tryLock()。
// 作用:boolean upgraded = lock.writeLock().tryLock();;不要在读锁内直接申请写锁。
System.out.println("upgraded=" + upgraded);
// 输出:upgraded=false
} finally {
lock.readLock().unlock();
// 初始状态:lock.readLock().unlock()。
// 作用:lock.readLock().unlock();;不要在读锁内直接申请写锁。
}
}
}读锁升级没有安全的原子操作:当前线程的读锁本身就是写锁必须等待的读者。示例用 tryLock() 立即失败来暴露风险;如果改成 writeLock().lock(),线程可能一直等自己释放读锁。需要写入时,应先释放读锁、再按固定策略申请写锁,并重新检查条件。
readLock().tryLock(timeout):有界等待读锁
调用方不能无限等待读锁时使用带超时的获取;超时后必须走降级、返回或重试策略。
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class ReadLockTimeoutDemo {
public static void main(String[] args) throws InterruptedException {
var lock = new ReentrantReadWriteLock();
// 初始状态:lock = new ReentrantReadWriteLock()。
// 作用:var lock = new ReentrantReadWriteLock();;有界等待读锁。
boolean acquired = lock.readLock().tryLock(1, TimeUnit.MILLISECONDS);
// 初始状态:acquired = lock.readLock().tryLock(1, TimeUnit.MILLISECONDS)。
// 作用:boolean acquired = lock.readLock().tryLock(1, TimeUnit.MILLISECONDS);;有界等待读锁。
try {
System.out.println("acquired=" + acquired);
// 输出:acquired=true(没有其他线程持有写锁时)
} finally {
if (acquired) {
lock.readLock().unlock();
// 初始状态:lock.readLock().unlock()。
// 作用:lock.readLock().unlock();;有界等待读锁。
}
}
}
}超时获取让调用方可以选择降级、重试或返回忙碌,而不是无限等待。获取失败时没有持有锁,不能无条件调用 unlock()。
writeLock().lockInterruptibly():可中断地等待写锁
线程需要响应取消信号时使用可中断的写锁获取;捕获中断异常后应恢复中断状态或向上层传播。
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class WriteLockInterruptibleDemo {
public static void main(String[] args) throws InterruptedException {
var lock = new ReentrantReadWriteLock();
// 初始状态:lock = new ReentrantReadWriteLock()。
// 作用:var lock = new ReentrantReadWriteLock();;可中断地等待写锁。
lock.writeLock().lockInterruptibly();
// 初始状态:lock.writeLock().lockInterruptibly()。
// 作用:lock.writeLock().lockInterruptibly();;可中断地等待写锁。
try {
System.out.println("write lock acquired");
// 输出:write lock acquired
} finally {
lock.writeLock().unlock();
// 初始状态:lock.writeLock().unlock()。
// 作用:lock.writeLock().unlock();;可中断地等待写锁。
}
}
}ReadLock 和 WriteLock 都实现 Lock,因此可使用 lockInterruptibly()。捕获 InterruptedException 后应由上层决定退出、恢复中断状态或转换为业务异常,不能悄悄吞掉取消信号。
getReadLockCount():只用于诊断当前读者数量
需要观测当前读者数量时使用该方法做诊断;返回值是瞬时估计,不能作为业务同步条件。
// 作用:通过 getReadLockCount() 只用于诊断当前读者数量。
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class ReadLockCountDemo {
public static void main(String[] args) {
var lock = new ReentrantReadWriteLock();
// 初始状态:lock = new ReentrantReadWriteLock()。
// 作用:var lock = new ReentrantReadWriteLock();;只用于诊断当前读者数量。
lock.readLock().lock();
// 初始状态:lock.readLock().lock()。
// 作用:lock.readLock().lock();;只用于诊断当前读者数量。
try {
System.out.println(lock.getReadLockCount());
// 输出:1
} finally {
lock.readLock().unlock();
// 初始状态:lock.readLock().unlock()。
// 作用:lock.readLock().unlock();;只用于诊断当前读者数量。
}
}
}getReadLockCount() 适合监控和诊断,不应作为业务判断依据;返回值可能在并发环境中随时变化。当前线程自己的重入次数可用 getReadHoldCount() 观察,但同样不应代替状态协议。
readLock().newCondition():读锁不支持 Condition
需要条件队列时应改用写锁或 ReentrantLock;读锁不支持 newCondition(),不能把读锁当作条件同步器。
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class ReadLockConditionDemo {
public static void main(String[] args) {
var lock = new ReentrantReadWriteLock();
// 初始状态:lock = new ReentrantReadWriteLock()。
// 作用:var lock = new ReentrantReadWriteLock();;读锁不支持 Condition。
try {
lock.readLock().newCondition();
// 初始状态:lock.readLock().newCondition()。
// 作用:lock.readLock().newCondition();;读锁不支持 Condition。
} catch (UnsupportedOperationException ex) {
System.out.println(ex.getClass().getSimpleName());
// 输出:UnsupportedOperationException
}
}
}读锁是共享锁,不能直接建立 Condition。需要条件等待时使用写锁的 newCondition(),或改用 ReentrantLock 的多个条件队列;条件状态必须和写入它的锁保持一致。
简单案例
读多写少缓存:读写路径分离
缓存命中远多于刷新且读临界区足够长时才考虑读写锁;先用基准确认收益,避免为单字段读取增加复杂度。
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class ReadWriteCacheDemo {
private final Map<String, String> cache = new HashMap<>();
// 初始状态:cache = new HashMap<>()。
// 作用:private final Map<String, String> cache = new HashMap<>();;读写路径分离。
private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
// 初始状态:lock = new ReentrantReadWriteLock()。
// 作用:private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();;读写路径分离。
String get(String key) {
lock.readLock().lock();
// 初始状态:lock.readLock().lock()。
// 作用:lock.readLock().lock();;读写路径分离,返回读取结果。
try {
return cache.get(key);
// 初始状态:return cache.get(key)。
// 作用:return cache.get(key);;读写路径分离,返回读取结果。
} finally {
lock.readLock().unlock();
// 初始状态:lock.readLock().unlock()。
// 作用:lock.readLock().unlock();;读写路径分离,返回读取结果。
}
}
void put(String key, String value) {
lock.writeLock().lock();
// 初始状态:lock.writeLock().lock()。
// 作用:lock.writeLock().lock();;读写路径分离。
try {
cache.put(key, value);
// 初始状态:cache.put(key, value)。
// 作用:cache.put(key, value);;读写路径分离。
} finally {
lock.writeLock().unlock();
// 初始状态:lock.writeLock().unlock()。
// 作用:lock.writeLock().unlock();;读写路径分离。
}
}
public static void main(String[] args) {
var cache = new ReadWriteCacheDemo();
// 初始状态:cache = new ReadWriteCacheDemo()。
// 作用:var cache = new ReadWriteCacheDemo();;读写路径分离,返回读取结果。
cache.put("language", "Java");
// 初始状态:cache.put("language", "Java")。
// 作用:cache.put("language", "Java");;读写路径分离。
System.out.println(cache.get("language"));
// 输出:Java
}
}这个模板只在读竞争确实存在时才有价值。缓存值若能用不可变对象整体替换,读锁甚至可以被安全发布或并发容器取代,减少锁协议的复杂度。
易混点
- 读锁是共享的,但写锁仍然独占;“读读不互斥”不等于“读写不互斥”。
- 锁降级可以先写锁再读锁;读锁升级不能直接
lock(),否则可能等待自己释放读锁。 - 公平读写锁只是改变排队倾向,不保证绝对先来先服务,也通常牺牲部分吞吐。
readLock().newCondition()不受支持;只有写锁可以建立Condition,因为条件等待需要独占地修改状态。- 读写锁不是读多写少的万能加速器;读临界区很短、写比例高或锁竞争不明显时,普通互斥锁可能更快更简单。
- 读写锁不会保护绕过它的字段访问;所有读写路径必须遵守同一个锁协议。
课后小问
为什么锁降级必须先获得读锁,再释放写锁? 答案:这样释放写锁后当前线程仍由读锁保护,不会在写锁和读锁之间暴露空档。 解析:如果先释放写锁再申请读锁,其他写者可能插入并修改状态,当前线程随后读到的就不一定是自己刚写入的版本。
为什么不能在持有读锁时直接申请写锁? 答案:读锁计数还包括当前线程自己,写锁需要等所有读者离开,直接升级可能把自己永久阻塞。 解析:需要升级时应释放读锁、申请写锁并重新检查条件;如果业务经常需要升级,通常应改用写锁或重新设计状态机。
什么时候普通
synchronized可能优于读写锁? 答案:读临界区很短、写操作不少、并发读竞争不明显,或代码更需要简单可靠的锁协议时。 解析:读写锁有额外的读者计数和排队成本,只有基准测试证明并发读收益时才值得引入。
本节小结
ReentrantReadWriteLock用共享读锁和独占写锁表达读写并发边界。- 写锁降级必须先获得读锁再释放写锁,读锁升级没有安全的直接路径。
- 公平策略可以降低插队机会,但会付出吞吐和排队成本。
- 读写锁只适合经过测量的读多写少场景,短临界区可能用普通互斥锁更合适。
快速回顾
- 会用
readLock()和writeLock()的lock/finally/unlock模板。 - 能写出“写锁内获取读锁,再释放写锁”的降级流程。
- 能解释读锁升级风险、公平策略和
Condition的边界。 - 会根据读写比例、临界区长度和基准结果决定是否使用读写锁。
阿源的知识库