// 位于Sync中
protected final int tryAcquireShared(int unused) {
Thread current = Thread.currentThread();
int c = getState();
if (exclusiveCount© != 0 &&
getExclusiveOwnerThread() != current)
return -1;
int r = sharedCount©;
if (!readerShouldBlock() &&
r < MAX_COUNT &&
compareAndSetState(c, c + SHARED_UNIT)) {
// 本线程是让读锁从0到1的线程
if (r == 0) {
firstReader = current;
firstReaderHoldCount = 1;
// 本线程重入方式获取,并且是第一个获取的线程
} else if (firstReader == current) {
firstReaderHoldCount++;
// 本线程不是第一个获取读锁的线程
} else {
HoldCounter rh = cachedHoldCounter;
if (rh == null ||
rh.tid != LockSupport.getThreadId(current))
cachedHoldCounter = rh = readHolds.get();
else if (rh.count == 0)
readHolds.set(rh);
rh.count++;
}
return 1;
}
return fullTryAcquireShared(current);
}
复制代码
调用关系比较简单,不用说了。主要关注tryAcquireShared
函数。首先是进行逻辑判断if (exclusiveCount(c) != 0 && getExclusiveOwnerThread() != current)
如果有其它线程持有写锁,那么返回 -1。自己持有写锁是没问题的,可以往下走。
接下来又是一个短路操作:if (!readerShouldBlock() && r < MAX_COUNT && compareAndSetState(c, c + SHARED_UNIT))
,当!readerShouldBlock()
和 r < MAX_COUNT
均为true,就会进行第三个判断,也就是CAS设置锁。当三个条件都为真,那么意味着锁设置成功了,会执行代码块里那段看起来不明觉厉的代码。当然三个条件都满足还是挺难的,所以如果这个逻辑表达式不成立,会调用fullTryAcquireShared(current)
进行进一步获取。可见,tryAcquireShared
只是进行一次尝试。
接下来看逻辑表达式为真的那一段代码。
private transient Thread firstReader;
private transient int firstReaderHoldCount;
static final class HoldCounter {
int count; // initially 0
// Use id, not reference, to avoid garbage retention
final long tid = LockSupport.getThreadId(Thread.currentThread());
}
static final class ThreadLocalHoldCounter
extends ThreadLocal {
public HoldCounter initialValue() {
return new HoldCounter();
}
}
private transient ThreadLocalHoldCounter readHolds;
private transient HoldCounter cachedHoldCounter;
复制代码
这是那段代码中出现的变量的定义。fistReader是用来记录第一个获取到读锁的线程,fitstReaderHoldCount是记录此线程的持有数(ReentrantReadWriteLock也是可重入的);readHolds是ThreadLocalHoldCounter的对象,而ThreadLocalHoldCounter是ThreadLocal的子类。这个ThreadLocal里装的是HoldCounter类的对象,这个HoldCounter类里分别是持有数量和持有线程的id。看起来真是挺晕的(((φ(◎ロ◎;)φ)))。
接下来分析这段代码,先是r=0分支,此时,本线程是第一个让此读锁计数从0到1的线程,所以进行firstReader和firstReaderHoldCount的设置;否则,如果这个读锁的第一个持有的线程就是本线程,那么直接++firstReaderCount即可,也很合理。这两个地方的代码也没有进行同步处理,因为r是之前的读锁值,在进入r=0分支时,CAS设置读锁状态已经成功,所以其它线程再进来读也肯定到不了r=0这个分支了;对于else if (firstReader == current)
分支,肯定也只有本线程=firstReader时,才能触发,这两个分支不存在和其它线程的竞争。
如果以上两个条件都不满足,那么这个线程就是第二个及以后获得读锁的线程。这个时候,这个线程的读锁计数就由它自己维护了。这个分支里的代码就是对此线程的读锁计数进行一番操作。首先是HoldCounter rh = cachedHoldCounter;
,有些书上说cachedHoldCounter是记录最后一个获取读锁的线程。我感觉也未必吧,毕竟这个变量也不是volatile的,无法保证可见性,你读到的未必就是真正最后一个获取的。先进行if (rh == null || rh.tid != LockSupport.getThreadId(current))
判断,如果不满足,也就是从cachedHoldCounter获取到的rh正好就是本线程的;如果rh不是本线程的,经过 cachedHoldCounter = rh = readHolds.get();
设置之后,rh也成为了本线程的HoldCounter变量。
之后是else if (rh.count == 0)
分支,如果可以进入这个分支,也就意味着cachedHoldCounter保存的HoldCounter对象确实是本线程的,但是对象里对应的count却为0。那么为什么会出现这种情况呢?因为读锁的释放过程并没有清除cachedHoldCounter的代码。所以是cachedHoldCounter对应的线程之前的读锁被释放过一次,这个线程又再次来获取读锁,所以把这个本来就属于它的HoldCounter变量再赋给它。
总之不管怎样,当执行到rh.count++;
这条语句时,rh对应的一定是本线程的HoldCount对象。把它的计数自增一个。
这块代码我看的时候属实难受啊,看了很久才看明白。其实没有firstReader,firstReaderHoldCount,cachedHoldCounter
也不是不行。反正HoldCounter是ThreadLocal的,每个线程都有,从自己线程读也可以。但是可能那样读取效率有些低,所以这里设置了一点相当于缓存的变量,如果这些变量命中了,就不需要去自己线程读了。你看它命名也能看出来:cachedHoldCounter
。
完全获取
final int fullTryAcquireShared(Thread current) {
HoldCounter rh = null;
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()) {
// Make sure we’re not acquiring read lock reentrantly
if (firstReader == current) {
// assert firstReaderHoldCount > 0;
} else {
if (rh == null) {
// 尝试先以cachedHoldCounter方式获取线程的HolderCounter对象
rh = cachedHoldCounter;
// 如果cachedHoldCounter没有获取到,再从ThreadLocal里获取
if (rh == null ||
rh.tid != LockSupport.getThreadId(current)) {
rh = readHolds.get();
if (rh.count == 0)
readHolds.remove();
}
}
if (rh.count == 0)
return -1;
}
}
if (sharedCount© == MAX_COUNT)
throw new Error(“Maximum lock count exceeded”);
// 进行CAS设置读锁计数
if (compareAndSetState(c, c + SHARED_UNIT)) {
if (sharedCount© == 0) {
firstReader = current;
firstReaderHoldCount = 1;
} else if (firstReader == current) {
firstReaderHoldCount++;
} else {
if (rh == null)
rh = cachedHoldCounter;
if (rh == null ||
rh.tid != LockSupport.getThreadId(current))
rh = readHolds.get();
else if (rh.count == 0)
readHolds.set(rh);
rh.count++;
cachedHoldCounter = rh; // cache for release
}
return 1;
}
}
}
复制代码
代码首先检查此锁的写锁是否被持有。从代码可以看出来,当写锁被本线程持有时,是可以再获取读锁的;如果是其它线程持有写锁,则返回-1。
接下来进入到else if (readerShouldBlock()) {
分支。进入此分支说明写锁没有被其它线程持有,但是这个线程获取读锁需要被阻塞。
// 非公平锁实现
final boolean readerShouldBlock() {
return apparentlyFirstQueuedIsExclusive();
}
// 公平锁实现
final boolean readerShouldBlock() {
return hasQueuedPredecessors();
}
复制代码
这都是AQS类里定义的函数,这里不细说了。那为啥这种情况还有一堆代码呢?为啥不直接返回-1?
if (rh.count == 0)
return -1;
复制代码
这块的关键代码其实在这。这部分是判断本线程是否已经持有了读锁,从源代码来看,Java的开发者认为如果是重入方式获取读锁,即使readerShouldBlock()
为真,也可以去下一部分获取。如果if (firstReader == current)
为真,那肯定是重入获取的,可以进行下一步;否则又是用 cachedHoldCounter 来尝试命中缓存,没有命中,就从自己线程本地读取 HoldCounter 对象,这块之前已经解释了。
如果这一段代码都没有return,那么说明这个线程可以允许获取读锁,于是进行CAS操作来设置读锁的状态。如果可以进入到if (compareAndSetState(c, c + SHARED_UNIT))
分支,说明已经获取成功了,和尝试获取类似,把线程对应 的HoldCounter 的计数自增一个。否则,注意到整个代码包在一个for (;;)
里,线程会不断尝试CAS操作。
读锁的获取还有一个tryReadLock()
,就是不断循环获取,代码基本一样。
public boolean tryLock() {
return sync.tryReadLock();
}
// 位于Sync中
final boolean tryReadLock() {
Thread current = Thread.currentThread();
for (;😉 {
int c = getState();
if (exclusiveCount© != 0 &&
getExclusiveOwnerThread() != current)
return false;
int r = sharedCount©;
if (r == MAX_COUNT)
throw new Error(“Maximum lock count exceeded”);
if (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 != LockSupport.getThreadId(current))
cachedHoldCounter = rh = readHolds.get();
else if (rh.count == 0)
readHolds.set(rh);
rh.count++;
}
return true;
}
}
}
复制代码
读锁的释放
public void unlock() {
sync.releaseShared(1);
}
// 位于AQS中
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
signalNext(head);
return true;
}
return false;
}
位于Sync中
protected final boolean tryReleaseShared(int unused) {
Thread current = Thread.currentThread();
if (firstReader == current) {
// assert firstReaderHoldCount > 0;
if (firstReaderHoldCount == 1)
firstReader = null;
else
firstReaderHoldCount–;
} else {
HoldCounter rh = cachedHoldCounter;
if (rh == null ||
rh.tid != LockSupport.getThreadId(current))
rh = readHolds.get();
int count = rh.count;
if (count <= 1) {
readHolds.remove();
if (count <= 0)
throw unmatchedUnlockException();
}
–rh.count;
}
for (;😉 {
int c = getState();
int nextc = c - SHARED_UNIT;
if (compareAndSetState(c, nextc))
// Releasing the read lock has no effect on readers,
// but it may allow waiting writers to proceed if
// both read and write locks are now free.
return nextc == 0;
}
}
复制代码
首先是if (firstReader == current)
分支,进入到这个分支说明本线程是第一个获得读锁的线程,直接操作firstReader和firstReaderHoldCounter即可。
if (firstReader == current)
对应的else分支又进行了一番操作,其实就是把线程对应的读锁计数-1。在此过程中,如果发现if (count <= 1)
,说明,这个锁计数即将为0,需要释放,所以进行了readHolds.remove();
操作,把线程的HoldCounter对象清除。如果发现if (count <= 0)
,此时根本没有可释放的东西,抛出了异常。
之后是循环CAS设置读锁状态。
其它方法
到此为止,读写锁里比较困难的代码已经解释完了,还有一些很简单的方法诸如以下:
public final boolean isFair() {
return sync instanceof FairSync;
}
…
protected Thread getOwner() {
return sync.getOwner();
}
// other methods
复制代码
这些都很简单,没什么可说的。
先自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。
深知大多数初中级Java工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则近万的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!
因此收集整理了一份《Java开发全套学习资料》送给大家,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频
如果你觉得这些内容对你有帮助,可以扫码领取!
![img](https://img-blog.csdnimg.cn/img_convert/1801ccea5c64bab2c6d02a8bf97f8dce.jpeg)
最后
总而言之,面试官问来问去,问的那些Redis知识点也就这么多吧,复习的不够到位,知识点掌握不够熟练,所以面试才会卡壳。将这些Redis面试知识解析以及我整理的一些学习笔记分享出来给大家参考学习
还有更多学习笔记面试资料也分享如下:
送给大家,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。**
[外链图片转存中…(img-DudDhPA1-1711383093780)]
[外链图片转存中…(img-UY5qZWTV-1711383093780)]
[外链图片转存中…(img-9DMD9SqV-1711383093781)]
由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频
如果你觉得这些内容对你有帮助,可以扫码领取!
![img](https://img-blog.csdnimg.cn/img_convert/1801ccea5c64bab2c6d02a8bf97f8dce.jpeg)
最后
总而言之,面试官问来问去,问的那些Redis知识点也就这么多吧,复习的不够到位,知识点掌握不够熟练,所以面试才会卡壳。将这些Redis面试知识解析以及我整理的一些学习笔记分享出来给大家参考学习
还有更多学习笔记面试资料也分享如下:
[外链图片转存中…(img-YL2VcKCr-1711383093781)]
需要更多Java资料的小伙伴可以帮忙点赞+关注,点击传送门,即可免费领取!