// 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 ThreadLocal 呢?(当然,答案肯定不在这块代码中)
// 既然 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,如果发现读锁和写锁都释放光了,那么唤醒后继的获取写锁的线程。
写锁获取
-
写锁是独占锁。
-
如果有读锁被占用,写锁获取是要进入到阻塞队列中等待的。
// 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;
}
看到这里,是不是发现写锁相对于读锁来说要简单很多。
锁降级
Doug Lea 没有说写锁更高级,如果有线程持有读锁,那么写锁获取也需要等待。
不过从源码中也可以看出,确实会给写锁一些特殊照顾,如非公平模式下,为了提高吞吐量,lock 的时候会先 CAS 竞争一下,能成功就代表读锁获取成功了,但是如果发现 head.next 是获取写锁的线程,就不会去做 CAS 操作。
Doug Lea 将持有写锁的线程,去获取读锁的过程称为锁降级(Lock downgrading)。这样,此线程就既持有写锁又持有读锁。
但是,锁升级是不可以的。线程持有读锁的话,在没释放的情况下不能去获取写锁,因为会发生死锁。
回去看下写锁获取的源码:
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;
总结
以上是字节二面的一些问题,面完之后其实挺后悔的,没有提前把各个知识点都复习到位。现在重新好好复习手上的面试大全资料(含JAVA、MySQL、算法、Redis、JVM、架构、中间件、RabbitMQ、设计模式、Spring等),现在起闭关修炼半个月,争取早日上岸!!!
下面给大家分享下我的面试大全资料
- 第一份是我的后端JAVA面试大全
后端JAVA面试大全
- 第二份是MySQL+Redis学习笔记+算法+JVM+JAVA核心知识整理
MySQL+Redis学习笔记算法+JVM+JAVA核心知识整理
- 第三份是Spring全家桶资料
MySQL+Redis学习笔记算法+JVM+JAVA核心知识整理
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;
总结
以上是字节二面的一些问题,面完之后其实挺后悔的,没有提前把各个知识点都复习到位。现在重新好好复习手上的面试大全资料(含JAVA、MySQL、算法、Redis、JVM、架构、中间件、RabbitMQ、设计模式、Spring等),现在起闭关修炼半个月,争取早日上岸!!!
下面给大家分享下我的面试大全资料
- 第一份是我的后端JAVA面试大全
[外链图片转存中…(img-m4bFRZXT-1723531649078)]
后端JAVA面试大全
- 第二份是MySQL+Redis学习笔记+算法+JVM+JAVA核心知识整理
[外链图片转存中…(img-MD2FRVAs-1723531649079)]
MySQL+Redis学习笔记算法+JVM+JAVA核心知识整理
- 第三份是Spring全家桶资料
[外链图片转存中…(img-exRC6BSH-1723531649079)]
MySQL+Redis学习笔记算法+JVM+JAVA核心知识整理