在之前的文章中,我们已经学习了synchronized和ReentrantLock。通过学习我们知道这两者都是属于排它锁,也叫做互斥锁。
什么叫互斥锁呢?就是不管哪个线程去读取数据还是修改数据,只要他持有锁,那其他的线程就必须等着。
这在大多数情况下肯定是线程安全的,没什么问题。
但是在实际的开发过程中,有一些很常见的读多写少的场景。像配置信息,热点数据的缓存。这些数据实际上被大量的线程进行频繁的读取,而只在极少的情况下会去修改。
在这类场景下,如果读取操作也使用排他锁,想想都会降低系统性能。因为读取操作本身并不会改变数据,多个线程同时读取同一个数据是完全安全的。
那有没有一种锁能把读取和写入区别开呢?
那肯定是有的,JUC包下的ReadWriteLock就是为解决这个问题而生的。
今天我们就一起来看看ReadWriteLock。
一、什么是ReadWriteLock
ReadWriteLock是java.util.concurrent.locks中的一个接口。既然是接口,那他就不是一把具体的锁。
可以把他理解成一个锁的管理者。他内部管理了两把相互关联的锁:
一个读取锁,也叫做共享锁。
一个写入所,也叫排它锁。
再提一句,源码中的注释真的有必要去看一看,不管是学习前还是学习后,尽量都去阅读下源码及注释。

ReentrantReadWriteLock是ReadWriteLock接口最常用的实现类。
这套锁机制有以下的核心原则:
读-读 共享:多个线程可以同时持有读取锁,进行并发读取。
读-写 互斥:当有线程持有读取锁时,写入线程必须等待。
写-读 互斥:当有线程持有写入锁时,所有其他线程(不管读写)都必须等待。
写-写 互斥:当有线程持有写入锁时,其他写入线程也必须等待。
其实很好理解,读锁大家用,写锁只能一个人用。
大家都是读,其乐融融,随便读。
我在读,你想写,你得等着,等我读完了,不然就存在边读边写的诡异事情。
我在写,你想读或者你也想写,那都得等着,不然又会混乱了。
简单的总结就是:读锁大家用,写锁我独占。
二、读写锁使用
我们通过一个小案例来看一下读写锁的使用:
java
package com.lazy.snail.day40;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
/**
* @ClassName Day40Demo
* @Description TODO
* @Author lazysnail
* @Date 2025/8/12 9:52
* @Version 1.0
*/
public class Day40Demo {
private final Map<String, String> map = new HashMap<>();
private final ReadWriteLock rwLock = new ReentrantReadWriteLock();
private final Lock rLock = rwLock.readLock();
private final Lock wLock = rwLock.writeLock();
public String get(String key) {
rLock.lock();
try { return map.get(key); }
finally { rLock.unlock(); }
}
public void put(String key, String value) {
wLock.lock();
try { map.put(key, value); }
finally { wLock.unlock(); }
}
}
之前的集合框架学习中,我们已经知道HashMap自身不是线程安全的,但是我们可以用锁把所有的访问都包起来。来保证线程安全。
rwLock是我们创建的读写锁管理器,生成两把关联的锁:读锁、写锁。
rLock和wLock分别取出读锁和写锁。
get方法代表只读路径,也就是读取。在没有线程持有写锁/等待写的时候,多个线程可以同时获取读锁。
在持锁期间进行读取(map.get()),避免了跟写线程并发导致的数据竞争。
最后在finally里面释放锁。
put方法代表写路径,也就是写入。
写锁是独占的。只要有线程持有写锁,其他读/写都得等;反过来,只要有读锁在,写锁也进不来。
唯一写入口(map.put),确保不会出现同时写或读写交叠。
同样在finally中释放锁。
三、内部原理
3.1 锁实现及获取
ReentrantReadWriteLock内部并不是维护两把完全独立的锁,而是用一个32位int变量state同时记录读写状态:
其中高16位表示读锁计数,允许多个线程同时增加。
低16位表示写锁计数,同一个线程可以重入。
ReentrantReadWriteLock中相关的核心常量:

获取写锁:
1.检查state是不是为0。如果不是0,意味着要么有读锁存在 (readCount > 0),要么已经有其他线程持有了写锁 (writeCount > 0)。
2.如果state不是0,但持有写锁的是当前线程,就可以重入,把writeCount加1。
3.如果state是0,那么当前线程可以获取写锁,把writeCount加1,把当前线程设置成锁的持有者。
获取读锁:
1.检查是不是存在写锁 (writeCount > 0),且持有写锁的不是当前线程。如果是,则获取失败,进入等待队列。
2.如果没有写锁,或者写锁被当前线程持有,那么当前线程可以获取读锁,通过CAS操作把readCount加1。
3.2 公平策略
ReentrantReadWriteLock跟ReentrantLock一样,也支持公平和非公平两种模式。
非公平锁:new ReentrantReadWriteLock()和new ReentrantReadWriteLock(false)都是非公平锁构造。允许新来的线程插队。举个例子,当写锁被释放的时候,如果等待队列里有读线程和写线程,同时又有一个新的读线程请求锁,那么这个新的读线程可能优先获得锁。从例子就能看出优点肯定是吞吐量更高,但可能会导致写线程饥饿。
公平锁:new ReentrantReadWriteLock(true)。这种模式就严格按照线程在等待队列里的FIFO顺序来分配锁。如果队列头部是写线程在等待,那所有后来的读锁请求都必须排队。优点就是能防止饥饿,保证公平性,一般情况下性能会低于非公平锁,毕竟要维持公平性。
3.3 锁升降级
锁降级指的是线程在持有写锁的情况下,继续获取读锁,然后释放写锁。
锁升级指的是线程在持有读锁的情况下,去获取写锁。ReentrantReadWriteLock是不支持的。
锁降级
考虑这样一个场景,如果有一个线程更新了共享数据后,还需要读取这些数据处理其他业务,但这个时候已经不希望再独占资源,而是允许其他读线程进来。
java
wLock.lock(); // 获取写锁
try {
// 修改数据...
map.put("name", "懒惰蜗牛");
// 锁降级:在持有写锁的情况下获取读锁
rLock.lock();
// 释放写锁(这个时候仍然持有读锁)
// 其他线程现在可以获取读锁了
wLock.unlock();
// 使用读锁状态读取数据,执行后续操作...
// 保证了在读取期间,数据不会被其他写线程修改
System.out.println("读取数据: " + map.get("name"));
} finally {
// 释放读锁
rLock.unlock();
}
锁升级
为什么不支持锁升级,如果两个线程(T1, T2)同时持有读锁,然后他们都尝试升级成写锁。T1要等待T2释放读锁,而T2也要等待T1释放读锁,就这样相互等着,最后就形成死锁了。
所以必须先rLock.unlock(),然后再wLock.lock()。但还是要注意的是,在释放读锁和获取写锁的间隙,数据可能被其他线程修改,原子性被破坏了,这就需要我们自己的业务逻辑来处理。
四、小结
目前我们已经接触了synchronized、ReentrantLock和ReentrantReadWriteLock。
下面对这三种形式的锁用表格进行清晰的对比:
| 特性 | synchronized | ReentrantLock | ReentrantReadWriteLock |
|---|---|---|---|
| 锁类型 | 互斥锁 | 互斥锁 | 读写分离锁(共享/互斥) |
| 底层实现 | JVM指令 | AQS | AQS |
| 公平性 | 非公平 | 可选(默认非公平) | 可选(默认非公平) |
| 可中断 | 不可中断 | 可中断 | 可中断 |
| 尝试获取锁 | 不支持 | 支持 (tryLock) | 支持 (tryLock) |
| Condition | wait/notify | Condition | 仅写锁支持Condition |
| 适用场景 | 简单同步代码块 | 需高级功能的复杂同步 | 读多写少的场景 |
每种形式的锁都有各自的优缺点,都有自己的应用场景。
结语
通过这几篇文章,初体验了一下Java中的synchronized、ReentrantLock和ReentrantReadWriteLock。
了解了锁在实际中的使用,对多线程场景下锁的应用也有了一定的了解。
下一篇文章我们一起来看下基于特性或者实现,到底有哪些锁的分类。
对于这些锁的分类有了一定了解后,我们继续后续关于锁的深入学习。
下一篇预告
Day41 | Java中的锁分类
如果你觉得这系列文章对你有帮助,欢迎关注专栏,我们一起坚持下去!
更多文章请关注我的公众号《懒惰蜗牛工坊》