Java精通并发-可重入读写锁的共享锁释放源码解析

前言:

在上一次Java精通并发-可重入读写锁底层源码分析及思想探究咱们对于ReadWriteLock的lock()上锁的细节从源码的角度进行了详情分析:

这次则来分析释放锁的底层源码:

读锁释放锁的逻辑分析:

ReentrantReadWriteLock.ReadLock.unlock():

这里还是以可重入的读锁为例进行分析:

从这代码大致也能猜到它里面的逻辑,就是释放锁,然后将读锁的计数器减1,跟上锁的逻辑刚好相反:

而对于这个sync,简单的再来回顾一下,毕竟距离上次的学习也过了有一阵了:

而它又是来源于这:

而默认是一个非公平锁,也就是此时的sync就是NonfairSync,然而它其实就是一个AQS:

嗯,反正就是跟AQS有关就成。

AbstractQueuedSynchronizer.releaseShared():

跟进去,此时就进入到了AQS当中了:

由于目前的这个arg是传的1,很明显只会释放一个线程的锁。所以接一下看一下tryReleaseShared()这个文中所提到的非常重要的一个条件:

很明显需要由子类来具体实现,毕竟不同的子类其AQS的行为是不一样的,所以此时则需要往此类的实现跟一下。

ReentrantReadWriteLock.Sync.tryReleaseShared():

哇,这方法有点复杂。。 晕晕的感觉,接下来一点点来分析吧,整体要分析就是这三块:

条件一:

先来查看一下firstReader的定义:

其中被transient所修饰了,也就是在序列化该对象时,此字段不参考序列化。那定义这变量的意义在于啥呢?先从官方解释上有一个大致的了解:

一大堆英文。。看着就头疼,直接找google翻译吧。。如果你想永远受翻译的牵制让英文阅读成为你的“被动”技能,那继续“快乐”的找翻译工具就成,但是!!!如果你想变被动为“主动”,而主动的含义就是指当面对一大段英文说明时你不再畏惧它,愿意主动的去带着平常所积累的英语阅读技能来理解它,而不是一看到它们就想到google翻译,连你本身都能读得懂的句子都懒得读,这块就不过多啰嗦了,智者见智仁者见仁,对我来说由于本身也开了一个专栏专门要攻克它:

所以,正好利用平常学习技能的同时来实践一下,有利无害,下面则来整体读一下,当然你肯定有遇到不懂的单词或读不懂的句子,此时再借助google翻译呗。

哦,从这句话就能明白:firstReader和firstReaderHoldCount是搭配使用的,用一个例子再来理解一下它们:比如有这么一个线程A,它是第一个获取读锁的线程【一定要明白,读锁是可以被多个线程同时获取的】,而它重入了三次【由于读锁是可重入的锁,可重入的意思就是说可以针对同一把锁获取多次】,此时firstReader指的就是这个线程A,而firstReaderHoldCount则等于3,而如果firstReader这个线程调用了一次unlock(),此时的firstReaderHoldCount就会由3变为2。

继续往下阅读:

啥意思呢?其实这里是说的跟垃圾回收相关的问题,这里不是单独定义了一个额外的成员变量来保存第一个获取读锁的线程么,那它不会产生内存泄漏的问题呀,这话题的意思是说正常情况下是不会产生的,因为只要线程正常的调用了unlock,最终都会主动将这个变量置为null的,除非该线程正在持有锁但是呢又挂掉了,此时这个变量就会变成一个垃圾滞留了,简言之,这句话表述的含义是:正常情况下就是不要担心这个变量会造成内存泄漏。

这句没明白说的是啥意思,先过,继续读最后一段:

好,读完之后,你就明白为啥要设计这么一个变量了,因为对于第一个获取读锁的线程在逻辑实现上是一个非常重要的数据,在了解了firstReader这个成员变量的含义之后,咱们就可以回到代码中进行理解了:

其中:

是不是就对应我们在阅读官方说明的这个:

另外,能进入到这个条件:

代表肯定该线程重复进行了读锁的获取了,也就是锁重入获取了,因为读锁是一个可重入的锁,这个大前提一定要明白。 

条件二:

接下来则是另一个条件的逻辑分析了:

通过这个代码也能明白,这个条件是指:当前释放锁的线程并不是第一个获取读锁的线程,有点绕,还是以一个例子说明一下,比如有两个线程A,B,A是第一个获取读锁的线程,而B是第二个获取读锁的线程,此时B调用了unlock(),此时就会进入到了这个条件了。

HoldCounter:

然后你会发现,第一句话就有点不好理解:

HoldCounter是个啥东东,得先来了解一下它才行:

其中说的cachedHoldCounter变量定义在这:

而它的赋值就是在上一次博文阅读密码验证 - 博客园分析上锁流程的这块代码:

只不过当时是草草带过了:

刚好通过这次的释放锁的逻辑再重新审视一下它,其中对于HoldCounter是这么设置的:

另外在上面的说明中说到HoldCounter是由ThreadLocal来进行维护的,哪里可以看出来呢?

它是继承自ThreadLocal的,然后initialValue就是HoldCounter,关于ThreadLocal的它的详细动作机制可以参考ThreadLocal流程深入分析 - cexo - 博客园,所以每一个线程就对应着一个HoldCounter,而它里面的count则是表示当前线程所持有的读锁的数量,而tid表示当前线程的ID,每一个HoldCounter都对应着一个读线程的实例,这么一挼,对于HoldCounter它的作用就比较清楚了。

接下来就可以继续源码的分析了:

接下来直接将HoldCounter的count减1既可:

但是!!!还存一个特殊情况,就是本身线程持有的读锁就只有一次,再释放锁那么整个计数就变为0了,也就代表该线程完全释放读锁了,所以就会执行这块代码了:

好, 这个条件里面有两个细节需要说明一下:

这个代码写得相当的不错,由于该线程已经不持有读锁了,那么线程对应readHolds的ThreadLocal对象是不是也没必要存在了,而在当时ThreadLocal内存泄露问题本质分析与代码编写最佳实践 - cexo - 博客园学习ThreadLocal的最佳实践中也提到过:

也就是当你的ThreadLocal没用时,最好要调用它的remove方法,可以防止内存泄漏,至于它的细节可以看一下参阅一下该文章,有详细说明,这也是体现你对ThreadLocal它的理解程度的一个非常重要的细节。

接着第二个细节就是它了:

想想,为啥?很明显这时你的程序调用肯定出问题了,而出现这种异常的情况有可能是你的lock()和unLock()调用是不匹配的,本身就是你程序逻辑出问题,那么这里当然就直接抛异常了喽。

死循环:

接下来就剩这个循环的逻辑了:

很明显,这个死循环不能真的让它无限死下去对吧,肯定是有一定的条件让其循环能终止掉,下面来看一下这块的逻辑,

而由于在ReentrantReadWriteLock中的state是由高低16位来表示读锁数量和写锁数量的:

其中关于compareAndSetState()它的含义可以参考Java精通并发-可重入锁对于AQS的实现源码分析,当读锁释放时,很明显:

而如果读锁还有被线程占据着,很明显这里就返回false了,而由于整个方法是有返回值的:

这样是不是这个死循环也会结束?关于这块的逻辑由于有位运算不太好理解的可以写个小程序自己调试一下就明白了,比如用这么一个代码:

debug你就会发现,这个死循环就返回false了。另外读一下循环体中的这段说明:

也就是,这个释放读锁的判断对于读线程是没有意义的,但是!!!对于写线程就有意义了,因为写线程是排它的呀,当写线程要获取锁时碰到了读线程被占据着是不是也得等待,然后当等待的读线程释放了,是不是写线程就“有机会”上锁了?注意!!!这里说的是“有机会”,并不是写线程一定就能获取,毕竟还需要跟其它线程进行资源竞争,如果写线程竞争成功了当然就可以成功获取锁了,但是如果跟它竞争的是读线程成功竞争到了那此时写线程还是会等待哟。

AbstractQueuedSynchronizer.doReleaseShared():

好,接下来就可以回到AQS继续往下分析了:

如果tryRelaseShared()条件为假,是不是直接返回false没有走到释放锁的逻辑对吧,而如果为真的,接下来就会执行到这个方法了:

先来读一读这个方法的说明:

也比较好理解,就是通知其它等待需要锁的线程来重新进行锁竞争,另外这方法里面还有一段说明:

先不读它,先来看一下这里面的代码逻辑吧。

这块在之前Java精通并发-可重入锁对于AQS的实现源码分析学习AQS其实已经学过了:

这是由于AQS中存在着一个阻塞的队列,它是由链表构成的双向队列,而链表当中每一个元素都是Node类型,里面会持有一个线程对象:

该线程就表示被阻塞的,被封装成了Node之后,它里面又有一个状态,所以你会看到有个这个条件:

而且你会发现,它是从链表的头结点开始找的:

所以,很明显就是找当前释放锁的下一个线程,通知它可以进行锁的竞争了,另外这个条件的意思能明白吧?

就是判断队列是否有元素,如果头和尾指针相同,证明队列肯定是没有元素的对吧。里面又有一个CAS的操作:

接下来,则会调用比较重要的方法了:

是不是在我们读注释说明时碰到过它:

那看一下它的细节:

在这方法最后有一句关键代码:

LockSupport从这名字来看是“锁的支持类”,然后你再点进去会发现,其实它都是jni方法:

此时要想再看里面的细节就需要翻阅openjdk的代码了,关于这块下次再来简单探索一下这个unpark在c++底层的实现细节,这样就将当前释放锁线程之后的等待线程给唤醒了。而doReleaseShared()里面代码还有另一个条件:

看注释表示CAS失败了,此时循环就继续,这块细节就不细扣了,重点是正常此方法会调到unparkSuccessor()通知指定的等待线程进行唤醒操作来让它进行锁的竞争。

最后,咱们再回过头来读一读这个注释:

看这段说明有点懵懵的,不过结合代码来看,多多少少有一点点理解吧,比如:

是不是设置成了PROPAGATE状态了,而如果失败,继续循环。

总结:

至此,对于读锁的释放细节就基本上已经梳理清楚了,那对于写锁的释放呢?

你会发现跟读锁的释放是一模一样的,所以这里就不重复分析了,剩下一个细节就是c++底层的唤醒操作这块了,也就是:

关于这块,下次从openjdk的源码中来探究一下。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

webor2006

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值