Java并发学习笔记:ReentrantReadWriteLock(良心长文(1)

}

复制代码

非公平锁直接返回false,公平锁调用AQS里的hasQueuedPredecessors();判断当前线程是否有前驱节点。这是一个逻辑表达式的短路操作。如果是非公平锁,if (writerShouldBlock() || !compareAndSetState(c, c + acquires))前一个条件返回false,那么要进行第二个条件的判断,尝试CAS设置锁;如果是公平锁,调用hasQueuedPredecessors();如果返回true,由于是||操作,后一个条件不用判断了,这个逻辑表达式直接返回true,否则,才会走下一个条件。如果把||改为|就不行了。这个功能如果让我写,我肯定是if if if,源代码的作者巧妙利用短路的操作,精简了代码,水平确实高啊。

public boolean tryLock() {

return sync.tryWriteLock();

}

final boolean tryWriteLock() {

Thread current = Thread.currentThread();

int c = getState();

if (c != 0) {

int w = exclusiveCount©;

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

return false;

if (w == MAX_COUNT)

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

}

if (!compareAndSetState(c, c + 1))

return false;

setExclusiveOwnerThread(current);

return true;

}

复制代码

还有tryLock( )。和普通的lock( )类似。不同在于:

  • 当c=0时,并没有调用writerShouldBlock()函数,直接进行了CAS设置锁的状态

  • 调用之后直接返回true or false,不会进入阻塞队列

写锁的释放

public void unlock() {

sync.release(1);

}

// 位于AQS中

public final boolean release(int arg) {

if (tryRelease(arg)) {

signalNext(head);

return true;

}

return false;

}

// 位于Sync中

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);

return free;

}

复制代码

这块和ReentrantLock类似。在tryRelease里先是查看当前线程是否真正持有锁,如果都没有持有,那还释放个啥。接着用free = exclusiveCount(nextc) == 0;来表示锁是不是释放干净了,如果是,在AQS里会signalNext(head);唤起下一个线程。

总的来说,写锁这部分和ReentrantLock类似,没什么太难的地方。

读锁

读锁的获取

尝试获取

public void lock() {

sync.acquireShared(1);

}

// 位于AQS中

public final void acquireShared(int arg) {

if (tryAcquireShared(arg) < 0)

acquire(null, arg, true, false, false, 0L);

}

// 位于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++;

}
自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数Java工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Java开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Java开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注Java获取)

img

最后

小编利用空余时间整理了一份《MySQL性能调优手册》,初衷也很简单,就是希望能够帮助到大家,减轻大家的负担和节省时间。

关于这个,给大家看一份学习大纲(PDF)文件,每一个分支里面会有详细的介绍。

image

这里都是以图片形式展示介绍,如要下载原文件以及更多的性能调优笔记(MySQL+Tomcat+JVM)!
《一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码》点击传送门即可获取!
涵盖了95%以上Java开发知识点,真正体系化!**

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注Java获取)

img

最后

小编利用空余时间整理了一份《MySQL性能调优手册》,初衷也很简单,就是希望能够帮助到大家,减轻大家的负担和节省时间。

关于这个,给大家看一份学习大纲(PDF)文件,每一个分支里面会有详细的介绍。

[外链图片转存中…(img-XHNxuHlH-1712125987901)]

这里都是以图片形式展示介绍,如要下载原文件以及更多的性能调优笔记(MySQL+Tomcat+JVM)!
《一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码》点击传送门即可获取!

  • 20
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值