Java 读写锁 ReentrantReadWriteLock 源码分析,还没搞懂JVM

Sync() {

// 初始化 readHolds 这个 ThreadLocal 属性

readHolds = new ThreadLocalHoldCounter();

// 为了保证 readHolds 的内存可见性

setState(getState()); // ensures visibility of readHolds

}

}

  1. state 的高 16 位代表读锁的获取次数,包括重入次数,获取到读锁一次加 1,释放掉读锁一次减 1

  2. state 的低 16 位代表写锁的获取次数,因为写锁是独占锁,同时只能被一个线程获得,所以它代表重入次数

  3. 每个线程都需要维护自己的 HoldCounter,记录该线程获取的读锁次数,这样才能知道到底是不是读锁重入,用 ThreadLocal 属性 readHolds 维护

  4. cachedHoldCounter 有什么用?其实没什么用,但能提示性能。将最后一次获取读锁的线程的 HoldCounter 缓存到这里,这样比使用 ThreadLocal 性能要好一些,因为 ThreadLocal 内部是基于 map 来查询的。但是 cachedHoldCounter 这一个属性毕竟只能缓存一个线程,所以它要起提升性能作用的依据就是:通常读锁的获取紧随着就是该读锁的释放。我这里可能表达不太好,但是大家应该是懂的吧。

  5. firstReader 和 firstReaderHoldCount 有什么用?其实也没什么用,但是它也能提示性能。将"第一个"获取读锁的线程记录在 firstReader 属性中,这里的第一个不是全局的概念,等这个 firstReader 当前代表的线程释放掉读锁以后,会有后来的线程占用这个属性的。firstReader 和 firstReaderHoldCount 使得在读锁不产生竞争的情况下,记录读锁重入次数非常方便快速

  6. 如果一个线程使用了 firstReader,那么它就不需要占用 cachedHoldCounter

  7. 个人认为,读写锁源码中最让初学者头疼的就是这几个用于提升性能的属性了,使得大家看得云里雾里的。主要是因为 ThreadLocal 内部是通过一个 ThreadLocalMap 来操作的,会增加检索时间。而很多场景下,执行 unlock 的线程往往就是刚刚最后一次执行 lock 的线程,中间可能没有其他线程进行 lock。还有就是很多不怎么会发生读锁竞争的场景。

上面说了这么多,是希望能帮大家降低后面阅读源码的压力,大家也可以先看看后面的,然后再慢慢体会。

前面我们好像都只说读锁,完全没提到写锁,主要是因为写锁真的是简单很多,我也特地将写锁的源码放到了后面,我们先啃下最难的读锁先。

读锁获取

下面我就不一行一行按源码顺序说了,我们按照使用来说。

我们来看下读锁 ReadLock 的 lock 流程:

// ReadLock

public void lock() {

sync.acquireShared(1);

}

// AQS

public final void acquireShared(int arg) {

if (tryAcquireShared(arg) < 0)

doAcquireShared(arg);

}

然后我们就会进到 Sync 类的 tryAcquireShared 方法:

在 AQS 中,如果 tryAcquireShared(arg) 方法返回值小于 0 代表没有获取到共享锁(读锁),大于 0 代表获取到

回顾 AQS 共享模式:tryAcquireShared 方法不仅仅在 acquireShared 的最开始被使用,这里是 try,也就可能会失败,如果失败的话,执行后面的 doAcquireShared,进入到阻塞队列,然后等待前驱节点唤醒。唤醒以后,还是会调用 tryAcquireShared 进行获取共享锁的。当然,唤醒以后再 try 是很容易获得锁的,因为这个节点已经排了很久的队了,组织是会照顾它的。

所以,你在看下面这段代码的时候,要想象到两种获取读锁的场景,一种是新来的,一种是排队排到它的。

protected final int tryAcquireShared(int unused) {

Thread current = Thread.currentThread();

int c = getState();

// exclusiveCount© 不等于 0,说明有线程持有写锁,

// 而且不是当前线程持有写锁,那么当前线程获取读锁失败

// (另,如果持有写锁的是当前线程,是可以继续获取读锁的)

if (exclusiveCount© != 0 &&

getExclusiveOwnerThread() != current)

return -1;

// 读锁的获取次数

int r = sharedCount©;

// 读锁获取是否需要被阻塞,稍后细说。为了进去下面的分支,假设这里不阻塞就好了

if (!readerShouldBlock() &&

// 判断是否会溢出 (2^16-1,没那么容易溢出的)

r < MAX_COUNT &&

// 下面这行 CAS 是将 state 属性的高 16 位加 1,低 16 位不变,如果成功就代表获取到了读锁

compareAndSetState(c, c + SHARED_UNIT)) {

// =======================

// 进到这里就是获取到了读锁

// =======================

if (r == 0) {

// r == 0 说明此线程是第一个获取读锁的,或者说在它前面获取读锁的都走光光了,它也算是第一个吧

// 记录 firstReader 为当前线程,及其持有的读锁数量:1

firstReader = current;

firstReaderHoldCount = 1;

} else if (firstReader == current) {

// 进来这里,说明是 firstReader 重入获取读锁(这非常简单,count 加 1 结束)

firstReaderHoldCount++;

} else {

// 前面我们说了 cachedHoldCounter 用于缓存最后一个获取读锁的线程

// 如果 cachedHoldCounter 缓存的不是当前线程,设置为缓存当前线程的 HoldCounter

HoldCounter rh = cachedHoldCounter;

if (rh == null || rh.tid != getThreadId(current))

cachedHoldCounter = rh = readHolds.get();

else if (rh.count == 0)

// 到这里,那么就是 cachedHoldCounter 缓存的是当前线程,但是 count 为 0,

// 大家可以思考一下:这里为什么要 set ThreadLo

【一线大厂Java面试题解析+后端开发学习笔记+最新架构讲解视频+实战项目源码讲义】

浏览器打开:qq.cn.hn/FTf 免费领取

cal 呢?(当然,答案肯定不在这块代码中)

// 既然 cachedHoldCounter 缓存的是当前线程,

// 当前线程肯定调用过 readHolds.get() 进行初始化 ThreadLocal

readHolds.set(rh);

// count 加 1

rh.count++;

}

// return 大于 0 的数,代表获取到了共享锁

return 1;

}

// 往下看

return fullTryAcquireShared(current);

}

上面的代码中,要进入 if 分支,需要满足:readerShouldBlock() 返回 false,并且 CAS 要成功(我们先不要纠结 MAX_COUNT 溢出)。

那我们反向推,怎么样进入到最后的 fullTryAcquireShared:

  • readerShouldBlock() 返回 true,2 种情况:

  • 在 FairSync 中说的是 hasQueuedPredecessors(),即阻塞队列中有其他元素在等待锁。

也就是说,公平模式下,有人在排队呢,你新来的不能直接获取锁

  • 在 NonFairSync 中说的是 apparentlyFirstQueuedIsExclusive(),即判断阻塞队列中 head 的第一个后继节点是否是来获取写锁的,如果是的话,让这个写锁先来,避免写锁饥饿。

作者给写锁定义了更高的优先级,所以如果碰上获取写锁的线程马上就要获取到锁了,获取读锁的线程不应该和它抢。

如果 head.next 不是来获取写锁的,那么可以随便抢,因为是非公平模式,大家比比 CAS 速度

  • compareAndSetState(c, c + SHARED_UNIT) 这里 CAS 失败,存在竞争。可能是和另一个读锁获取竞争,当然也可能是和另一个写锁获取操作竞争。

然后就会来到 fullTryAcquireShared 中再次尝试:

/**

* 1. 刚刚我们说了可能是因为 CAS 失败,如果就此返回,那么就要进入到阻塞队列了,

* 想想有点不甘心,因为都已经满足了 !readerShouldBlock(),也就是说本来可以不用到阻塞队列的,

* 所以进到这个方法其实是增加 CAS 成功的机会

* 2. 在 NonFairSync 情况下,虽然 head.next 是获取写锁的,我知道它等待很久了,我没想和它抢,

* 可是如果我是来重入读锁的,那么只能表示对不起了

*/

final int fullTryAcquireShared(Thread current) {

HoldCounter rh = null;

// 别忘了这外层有个 for 循环

for (;😉 {

int c = getState();

// 如果其他线程持有了写锁,自然这次是获取不到读锁了,乖乖到阻塞队列排队吧

if (exclusiveCount© != 0) {

if (getExclusiveOwnerThread() != current)

return -1;

// else we hold the exclusive lock; blocking here

// would cause deadlock.

} else if (readerShouldBlock()) {

/**

* 进来这里,说明:

* 1. exclusiveCount© == 0:写锁没有被占用

* 2. readerShouldBlock() 为 true,说明阻塞队列中有其他线程在等待

*

* 既然 should block,那进来这里是干什么的呢?

* 答案:是进来处理读锁重入的!

*

*/

// firstReader 线程重入读锁,直接到下面的 CAS

if (firstReader == current) {

// assert firstReaderHoldCount > 0;

} else {

if (rh == null) {

rh = cachedHoldCounter;

if (rh == null || rh.tid != getThreadId(current)) {

// cachedHoldCounter 缓存的不是当前线程

// 那么到 ThreadLocal 中获取当前线程的 HoldCounter

// 如果当前线程从来没有初始化过 ThreadLocal 中的值,get() 会执行初始化

rh = readHolds.get();

// 如果发现 count == 0,也就是说,纯属上一行代码初始化的,那么执行 remove

// 然后往下两三行,乖乖排队去

if (rh.count == 0)

readHolds.remove();

}

}

if (rh.count == 0)

// 排队去。

return -1;

}

/**

* 这块代码我看了蛮久才把握好它是干嘛的,原来只需要知道,它是处理重入的就可以了。

* 就是为了确保读锁重入操作能成功,而不是被塞到阻塞队列中等待

*

* 另一个信息就是,这里对于 ThreadLocal 变量 readHolds 的处理:

* 如果 get() 后发现 count == 0,居然会做 remove() 操作,

* 这行代码对于理解其他代码是有帮助的

*/

}

if (sharedCount© == MAX_COUNT)

throw new Error(“Maximum lock count exceeded”);

if (compareAndSetState(c, c + SHARED_UNIT)) {

// 这里 CAS 成功,那么就意味着成功获取读锁了

// 下面需要做的是设置 firstReader 或 cachedHoldCounter

if (sharedCount© == 0) {

// 如果发现 sharedCount© 等于 0,就将当前线程设置为 firstReader

firstReader = current;

firstReaderHoldCount = 1;

} else if (firstReader == current) {

firstReaderHoldCount++;

} else {

// 下面这几行,就是将 cachedHoldCounter 设置为当前线程

if (rh == null)

rh = cachedHoldCounter;

if (rh == null || rh.tid != getThreadId(current))

rh = readHolds.get();

else if (rh.count == 0)

readHolds.set(rh);

rh.count++;

cachedHoldCounter = rh;

}

// 返回大于 0 的数,代表获取到了读锁

return 1;

}

}

}

firstReader 是每次将读锁获取次数从 0 变为 1 的那个线程。

能缓存到 firstReader 中就不要缓存到 cachedHoldCounter 中。

上面的源码分析应该说得非常详细了,如果到这里你不太能看懂上面的有些地方的注释,那么可以先往后看,然后再多看几遍。

读锁释放

下面我们看看读锁释放的流程:

// ReadLock

public void unlock() {

sync.releaseShared(1);

}

// Sync

public final boolean releaseShared(int arg) {

if (tryReleaseShared(arg)) {

doReleaseShared(); // 这句代码其实唤醒 获取写锁的线程,往下看就知道了

return true;

}

return false;

}

// Sync

protected final boolean tryReleaseShared(int unused) {

Thread current = Thread.currentThread();

if (firstReader == current) {

if (firstReaderHoldCount == 1)

// 如果等于 1,那么这次解锁后就不再持有锁了,把 firstReader 置为 null,给后来的线程用

// 为什么不顺便设置 firstReaderHoldCount = 0?因为没必要,其他线程使用的时候自己会设值

firstReader = null;

else

firstReaderHoldCount–;

} else {

// 判断 cachedHoldCounter 是否缓存的是当前线程,不是的话要到 ThreadLocal 中取

HoldCounter rh = cachedHoldCounter;

if (rh == null || rh.tid != getThreadId(current))

rh = readHolds.get();

int count = rh.count;

if (count <= 1) {

// 这一步将 ThreadLocal remove 掉,防止内存泄漏。因为已经不再持有读锁了

readHolds.remove();

if (count <= 0)

// 就是那种,lock() 一次,unlock() 好几次的逗比

throw unmatchedUnlockException();

}

// count 减 1

–rh.count;

}

for (;😉 {

int c = getState();

// nextc 是 state 高 16 位减 1 后的值

int nextc = c - SHARED_UNIT;

if (compareAndSetState(c, nextc))

// 如果 nextc == 0,那就是 state 全部 32 位都为 0,也就是读锁和写锁都空了

// 此时这里返回 true 的话,其实是帮助唤醒后继节点中的获取写锁的线程

return nextc == 0;

}

}

读锁释放的过程还是比较简单的,主要就是将 hold count 减 1,如果减到 0 的话,还要将 ThreadLocal 中的 remove 掉。

然后是在 for 循环中将 state 的高 16 位减 1,如果发现读锁和写锁都释放光了,那么唤醒后继的获取写锁的线程。

写锁获取

  1. 写锁是独占锁。

  2. 如果有读锁被占用,写锁获取是要进入到阻塞队列中等待的。

// WriteLock

public void lock() {

sync.acquire(1);

}

// AQS

public final void acquire(int arg) {

if (!tryAcquire(arg) &&

// 如果 tryAcquire 失败,那么进入到阻塞队列等待

acquireQueued(addWaiter(Node.EXCLUSIVE), arg))

selfInterrupt();

}

// Sync

protected final boolean tryAcquire(int acquires) {

Thread current = Thread.currentThread();

int c = getState();

int w = exclusiveCount©;

if (c != 0) {

// 看下这里返回 false 的情况:

// c != 0 && w == 0: 写锁可用,但是有线程持有读锁(也可能是自己持有)

// c != 0 && w !=0 && current != getExclusiveOwnerThread(): 其他线程持有写锁

// 也就是说,只要有读锁或写锁被占用,这次就不能获取到写锁

if (w == 0 || current != getExclusiveOwnerThread())

return false;

if (w + exclusiveCount(acquires) > MAX_COUNT)

throw new Error(“Maximum lock count exceeded”);

// 这里不需要 CAS,仔细看就知道了,能到这里的,只可能是写锁重入,不然在上面的 if 就拦截了

setState(c + acquires);

return true;

}

// 如果写锁获取不需要 block,那么进行 CAS,成功就代表获取到了写锁

if (writerShouldBlock() ||

!compareAndSetState(c, c + acquires))

return false;

setExclusiveOwnerThread(current);

return true;

}

下面看一眼 writerShouldBlock() 的判定,然后你再回去看一篇写锁获取过程。

static final class NonfairSync extends Sync {

// 如果是非公平模式,那么 lock 的时候就可以直接用 CAS 去抢锁,抢不到再排队

final boolean writerShouldBlock() {

return false; // writers can always barge

}

}

static final class FairSync extends Sync {

final boolean writerShouldBlock() {

// 如果是公平模式,那么如果阻塞队列有线程等待的话,就乖乖去排队

return hasQueuedPredecessors();

}

}

写锁释放

// WriteLock

public void unlock() {

sync.release(1);

}

// AQS

public final boolean release(int arg) {

// 1. 释放锁

if (tryRelease(arg)) {

// 2. 如果独占锁释放"完全",唤醒后继节点

Node h = head;

if (h != null && h.waitStatus != 0)

unparkSuccessor(h);

return true;

}

return false;

}

// Sync

// 释放锁,是线程安全的,因为写锁是独占锁,具有排他性

// 实现很简单,state 减 1 就是了

protected final boolean tryRelease(int releases) {

if (!isHeldExclusively())

throw new IllegalMonitorStateException();

int nextc = getState() - releases;

boolean free = exclusiveCount(nextc) == 0;

if (free)

setExclusiveOwnerThread(null);

setState(nextc);

// 如果 exclusiveCount(nextc) == 0,也就是说包括重入的,所有的写锁都释放了,

// 那么返回 true,这样会进行唤醒后继节点的操作。

return free;

}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值