读写锁 ReentrantReadWriteLock 原理
产生背景
已经有了 ReentrantLock ,为什么还来一个 ReentrantReadWriteLock?
- ReentrantLock 是独占锁,哪怕所有线程都是读操作,其实不用加锁,但是 ReentrantLock 一次还是只能让一个线程获取,ReentrantReadWriteLock 就是解决这个问题的
- 简言之就是在读多的场景 ReentrantReadWriteLock 在读多的场景吞吐量比 ReentrantLock 好。技术是不断发展的,ReentrantReadWriteLock 也不完美,后面还有邮戳锁 StampedLock
读写锁定义:一个资源能同时被多个读线程访问,或者一个写线程访问,不能同时存在读写线程
读读共享
public class Test {
public static void main(String[] args) {
Resource resource = new Resource();
new Thread(() -> {
resource.read();
}, "线程A").start();
new Thread(() -> {
resource.read();
}, "线程B").start();
}
}
class Resource{
private final ReentrantReadWriteLock rrw = new ReentrantReadWriteLock();
private final ReentrantReadWriteLock.ReadLock readLock = rrw.readLock();
private final ReentrantReadWriteLock.WriteLock writeLock = rrw.writeLock();
public void read(){
readLock.lock();
System.out.println("获取读锁");
try {
System.out.println("读取数据....");
TimeUnit.SECONDS.sleep(1L);
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
System.out.println("读取完成,释放读锁");
readLock.unlock();
}
}
}
读写排他
public class Test {
public static void main(String[] args) {
Resource resource = new Resource();
new Thread(() -> {
resource.read();
}, "线程A").start();
// 休眠 100 毫秒
try {
TimeUnit.MILLISECONDS.sleep(100L);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
// 获取写锁,要等读锁释放才能获取
new Thread(() -> {
resource.write();
}, "线程B").start();
}
}
class Resource{
private final ReentrantReadWriteLock rrw = new ReentrantReadWriteLock();
private final ReentrantReadWriteLock.ReadLock readLock = rrw.readLock();
private final ReentrantReadWriteLock.WriteLock writeLock = rrw.writeLock();
public void read(){
readLock.lock();
System.out.println("获取读锁");
try {
System.out.println("读取数据....");
TimeUnit.SECONDS.sleep(1L);
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
System.out.println("读取完成,释放读锁");
readLock.unlock();
}
}
public void write(){
writeLock.lock();
System.out.println("获取写锁");
try {
System.out.println("写入数据....");
TimeUnit.SECONDS.sleep(1L);
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
System.out.println("写入完成,释放写锁");
writeLock.unlock();
}
}
}
锁降级
JDK ReentrandReadWriteLock 的一个示例
class CachedData {
Object data;
volatile boolean cacheValid; // volatile 变量,true:缓存可使用;false:缓存不可使用(需要初始化)
final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
void processCachedData() {
rwl.readLock().lock(); // 获取读锁
if (!cacheValid) { // 如果缓存可使用,这个 if 不走,直接走下面的 user 方法,使用缓存。如果为 false,说明缓存不可用,要初始化它
// Must release read lock before acquiring write lock
rwl.readLock().unlock(); // 要先释放读锁,因为不能在读锁中获取写锁
rwl.writeLock().lock(); // 获取写锁
try {
// Recheck state because another thread might have
// acquired write lock and changed state before we did.
if (!cacheValid) { // 再次判断缓存,双重检查的意味
data = ... // 维护缓存
cacheValid = true; // 维护后,缓存状态为可用
}
// Downgrade by acquiring read lock before releasing write lock
rwl.readLock().lock(); // 获取读锁,当前是持有写锁的,写锁里可以获取读锁,这就是锁降级(但是不能反着来,也就是读锁里不能获取写锁)
} finally {
// 释放写锁
rwl.writeLock().unlock(); // Unlock write, still hold read
}
}
try {
// 使用缓存
use(data);
} finally {
// 释放读锁
rwl.readLock().unlock();
}
}
}
原理
类结构
- 类结构上和 ReentrantLock 类似,也是有三个内部类:Sync(AQS)、NonfairSync(非公平锁)、FairSync(公平锁)
- 类结构上和 ReentrantLock 不同的是还有自己额外的两个内部类:WriteLock(写锁)、ReadLock(读锁)
ReentrantLock 底层依靠 AQS 实现,自己(子类 Sync )处理加锁释放锁,ReentrantReadWriteLock 也是一样的。区别也就是在这里
获取写锁
// java.util.concurrent.locks.ReentrantReadWriteLock.WriteLock#lock
public void lock() {
sync.acquire(1);
}
// java.util.concurrent.locks.AbstractQueuedSynchronizer#acquire
public final void acquire(int arg) {
if (!tryAcquire(arg) && // 获取锁,自己实现
acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) // AQS 入队
selfInterrupt();
}
// java.util.concurrent.locks.AbstractQueuedSynchronizer#tryAcquire
protected boolean tryAcquire(int arg) { // 模板方法
throw new UnsupportedOperationException();
}
// java.util.concurrent.locks.ReentrantReadWriteLock.Sync#tryAcquire
protected final boolean tryAcquire(int acquires) {
Thread current = Thread.currentThread();
int c = getState(); // 资源状态(可能读也可能写)
int w = exclusiveCount(c); // 写锁状态
if (c != 0) { // 资源已经被持有(可能读也可嫩写)
// 如果 w = 0,但是 c = 0,说明当前持有的是读锁,所以不允许获取写锁,但是如果是同一个线程可以重入
if (w == 0 || current != getExclusiveOwnerThread())
return false; // 获取锁失败,后面就要入队了,AQS 实现
// 走到这里 w !=0 且 c!=0 且 就是同一个线程,重入,但是不能重入最大值(65535),不然报错
if (w + exclusiveCount(acquires) > MAX_COUNT) //
throw new Error("Maximum lock count exceeded");
// Reentrant acquire
setState(c + acquires); // 修改 state
return true;
}
// 走到这里是 c == 0 的场景,资源未被持有,可以入
if (writerShouldBlock() || // 需要阻塞不,公平锁直接入队,如果是非公平锁要尝试插队
!compareAndSetState(c, c + acquires))
return false;
setExclusiveOwnerThread(current);
return true;
}
获取读锁
// java.util.concurrent.locks.ReentrantReadWriteLock.ReadLock#lock
public void lock() {
sync.acquireShared(1);
}
// java.util.concurrent.locks.AbstractQueuedSynchronizer#acquireShared
public final void acquireShared(int arg) {
// tryAcquireShared 方法根据返回值获取锁(可能返回 -1,0,1,对于读写锁只返回 +1 和 -1)
if (tryAcquireShared(arg) < 0) // -1 表示获取锁失败,+1 表示获取锁成功
doAcquireShared(arg); // 如果获取锁失败走这里
}
// java.util.concurrent.locks.AbstractQueuedSynchronizer#tryAcquireShared
protected int tryAcquireShared(int arg) { // 模板方法
throw new UnsupportedOperationException();
}
// java.util.concurrent.locks.ReentrantReadWriteLock.Sync#tryAcquireShared
protected final int tryAcquireShared(int unused) {
Thread current = Thread.currentThread();
int c = getState(); // 资源状态
if (exclusiveCount(c) != 0 && // 判断是否有线程持有写锁
getExclusiveOwnerThread() != current) // 写锁线程是不是自己
return -1; // 写锁已被别的线程持有,不能获取读锁,返回 -1,获取锁失败(锁降级)
// 如果没有线程持有写锁,可以获取读锁
int r = sharedCount(c); // 先获取已经得到读锁的线程数
if (!readerShouldBlock() &&
r < MAX_COUNT &&
compareAndSetState(c, c + SHARED_UNIT)) {
if (r == 0) {
firstReader = current;
firstReaderHoldCount = 1;
} else if (firstReader == current) {
firstReaderHoldCount++;
} else {
HoldCounter rh = cachedHoldCounter;
if (rh == null || rh.tid != getThreadId(current))
cachedHoldCounter = rh = readHolds.get();
else if (rh.count == 0)
readHolds.set(rh);
rh.count++;
}
return 1;
}
return fullTryAcquireShared(current);
}
// java.util.concurrent.locks.AbstractQueuedSynchronizer#doAcquireShared
private void doAcquireShared(int arg) {
// 注意是封装出来的节点类型是 Node.SHARED
final Node node = addWaiter(Node.SHARED); // 和 ReentrantLock 一样,如果队列未初始化这里也要初始化(虚拟节点和第二个节点),如果队列已经有节点了,排在后面
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor(); // 当前节点上一个节点
if (p == head) { // 如果是头节点,和 ReentrantLock 逻辑一样
int r = tryAcquireShared(arg);
if (r >= 0) {
setHeadAndPropagate(node, r);
p.next = null; // help GC
if (interrupted)
selfInterrupt();
failed = false;
return;
}
}
if (shouldParkAfterFailedAcquire(p, node) && // 和 ReentrantLock 一样,是否需要阻塞(死循环,一定会返回 true)
parkAndCheckInterrupt()) // park 线程
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· AI技术革命,工作效率10个最佳AI工具