ReentrantLock读写锁源码解析
ReentrantLock 流程
读写锁用的是同一个 Sycn 同步器,因此等待队列、state 等也是同一个 , 我们分别以下面的为例 :
- t1 -> w.lock t2 -> r.lock
t1 w.lock,t2 r.lock
注意 : ReentrantLock 和 ReentrantReadWriteLock默认都是非公平锁
测试代码
- t1 -> w.lock t2 -> r.lock
package cn.knightzz.juc.reentrantlock.readwrite.source;
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantReadWriteLock;
/**
* @author 王天赐
* @title: WriteAndReadLock
* @projectName hm-juc-codes
* @description:
* @website <a href="http://knightzz.cn/">http://knightzz.cn/</a>
* @github <a href="https://github.com/knightzz1998">https://github.com/knightzz1998</a>
* @create: 2022-09-21 09:03
*/
@SuppressWarnings("all")
@Slf4j(topic = "c.WriteAndReadLock")
public class WriteAndReadLock {
static ReentrantReadWriteLock rw = new ReentrantReadWriteLock();
static ReentrantReadWriteLock.ReadLock readLock = rw.readLock();
static ReentrantReadWriteLock.WriteLock writeLock = rw.writeLock();
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
log.debug("尝试获取写锁");
writeLock.lock();
try {
log.debug("获取写锁成功, 开始写入数据...");
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
writeLock.unlock();
}
}, "t1");
Thread t2 = new Thread(() -> {
log.debug("尝试获取读锁");
readLock.lock();
try {
log.debug("获取读锁成功, 开始读取数据...");
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
readLock.unlock();
}
}, "t2");
t1.start();
t2.start();
}
}
基本流程
[1] t1 成功上锁,流程与 ReentrantLock
加锁相比没有特殊之处,不同是写锁状态占了 state
的低 16 位,而读锁 使用的是 state
的高 16 位
![image-20220921152426422](https://i-blog.csdnimg.cn/blog_migrate/1a1ec36bb1b96feabd6aadf16e5baf69.png)
[2] t2 执行 readLock.lock()
,这时进入读锁的 sync.acquireShared(1)
流程,首先会进入 tryAcquireShared
流程。如果有写锁占据,那么 tryAcquireShared
返回 -1 表示失败
tryAcquireShared
返回值表示 : -1 表示失败 0 表示成功,但后继节点不会继续唤醒 , 正数表示成功,而且数值是还有几个后继节点需要唤醒,读写锁返回 1
// ReentrantReadWriteLock
public void lock() {
sync.acquireShared(1);
}
// Sync 继承自 AQS
public final void acquireShared(int arg) {
if (tryAcquireShared(arg) < 0)
doAcquireShared(arg);
}
// ReentrantReadWriteLock
protected final int tryAcquireShared(int unused) {
Thread current = Thread.currentThread();
// 获取 state
int c = getState();
// 判断前 16位是否是0(读锁是否被持有)
if (exclusiveCount(c) != 0 &&
// 判断持有锁的是否是当前线程
getExclusiveOwnerThread() != current)
return -1;
// 获取后16位状态 (写锁是否被持有)
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);
}
![image-20220921163852783](https://i-blog.csdnimg.cn/blog_migrate/0989d3d225cf067ab9254d9f670b2dbf.png)
[3] 这时会进入 sync.doAcquireShared(1)
流程,首先也是调用 addWaiter 添加节点,不同之处在于节点被设置为 Node.SHARED
模式而非 Node.EXCLUSIVE
模式,注意此时 t2 仍处于活跃状态
![image-20220922100621817](https://i-blog.csdnimg.cn/blog_migrate/95d58c172d3eca943e28b83b7c0c4e83.png)
[4] t2 会看看自己的节点是不是老二,如果是,还会再次调用 tryAcquireShared(1) 来尝试获取锁
[5] 如果没有成功,在 doAcquireShared
内 for (;😉 循环一次,把前驱节点的 waitStatus 改为 -1,再 for (;;)
循环一次尝试 tryAcquireShared(1) 如果还不成功,那么在parkAndCheckInterrupt()
处 park
![image-20220922100605017](https://i-blog.csdnimg.cn/blog_migrate/379e3cd116393d81922a1018e35424fb.png)
t3 r.lock,t4 w.lock
这种状态下,假设又有 t3 加读锁和 t4 加写锁,这期间 t1 仍然持有锁,就变成了下面的样子
![image-20220922101245437](https://i-blog.csdnimg.cn/blog_migrate/ad5a9a4233639df6e981bb305fa7b6ef.png)
t1 w.unlock
这时会走到写锁的 sync.release(1) 流程,调用 sync.tryRelease(1) 成功,变成下面的样子
接下来执行唤醒流程 sync.unparkSuccessor,即让老二恢复运行,这时 t2 在 doAcquireShared 内 parkAndCheckInterrupt() 处恢复运行
这回再来一次 for (;😉 执行 tryAcquireShared 成功则让读锁计数加一
这时 t2 已经恢复运行,接下来 t2 调用 setHeadAndPropagate(node, 1),它原本所在节点被置为头节点
![image-20220922104258197](https://i-blog.csdnimg.cn/blog_migrate/2b971a28edc77c3399740c99d993ae51.png)
事情还没完,在 setHeadAndPropagate 方法内还会检查下一个节点是否是 shared,如果是则调用 doReleaseShared() 将 head 的状态从 -1 改为 0 并唤醒老二,这时 t3 在 doAcquireShared 内
parkAndCheckInterrupt() 处恢复运行
![image-20220922105214421](https://i-blog.csdnimg.cn/blog_migrate/2e77cce14af4171d6f0201055bf60631.png)
下一个节点不是 shared 了,因此不会继续唤醒 t4 所在节点
t2 r.unlock,t3 r.unlock
t2 进入 sync.releaseShared(1) 中,调用 tryReleaseShared(1) 让计数减一,但由于计数还不为零
![image-20220922111151641](https://i-blog.csdnimg.cn/blog_migrate/73f9fec49a7f57a43aa1e195d5426855.png)
t3 进入 sync.releaseShared(1) 中,调用 tryReleaseShared(1) 让计数减一,这回计数为零了,进入 doReleaseShared() 将头节点从 -1 改为 0 并唤醒老二,即
![image-20220922110133074](https://i-blog.csdnimg.cn/blog_migrate/afb6fdc41a940acabb23f61fb0566603.png)
之后 t4 在 acquireQueued 中 parkAndCheckInterrupt 处恢复运行,再次 for (;😉 这次自己是老二,并且没有其他竞争,tryAcquire(1) 成功,修改头结点,流程结束
![image-20220922112535531](https://i-blog.csdnimg.cn/blog_migrate/59841d5a895a6c5d3ed14a2bcafbdc35.png)