Java精通并发-可重入读写锁对于AQS的实现分析

前言:

距离上一次Java精通并发-可重入锁对于AQS的实现源码分析java并发的学习又过去半年之久了,为了继续往下学习,还特意的温习了一下之前所记录的笔记 -----AQS,很快地就能回忆起当时学它时的各种细节,有了当年笔记的备忘,就能非常好地解决大脑不够用造成彻底断篇又得重头学习的尬镜,于我而言就是所谓的“好记性不如烂笔头”吧。

言归正传,上一次学习了关于ReentrantLock它对于AQS的使用,而当时主要集中在了获取锁的角度上,回忆一下当时的一个DEMO代码:

而整体获取锁的逻辑就是:当获取到锁之后当然线程就可以正常执行,而如果木有获取到锁则会增加到等待队列当中,等待着被唤醒:

ReentrantLock.unlock()流程剖析:

目前咱们已经学习了锁的获取流程了,那接下来则来了解一下锁的释放过程,也就是:

试想一下,对于AQS而言它有一个阻塞队列,那么对于阻塞队列中的这些线程什么时候会被唤醒呢?从使用的角度来看很明显应该是在调用了unlock()线程会释放锁,如果它里面的state值回归为0就说明此锁未被线程占用,所以此时处于等待队列中的线程就会从第一个按顺序尝试着获取这把锁,那这一切的触发就是调用了unlock(),因为只有调用了unlock()之后,线程才会释放锁,所以等待队列中的线程也才能有机会得到锁得以正常的执行。基于这种试想,咱们到源码中来验证一下:

ReentrantLock.unlock():

跟进去:

AbstractQueuedSynchronizer.release():

首先是一个条件判断:

ReentrantLock.Sync.tryRelease():

1、state值减1:

2、判断获取与释放锁的类型是否一致:

这点也很容易想明白,不可能我获取锁是线程A,而释放锁是线程B,所以做此判断的目的也很顺其自然。

3、如果state=0则释放排它锁:

而调用setExclusiveOwnerThread的地方还记得么,就是在lock()时,回忆一下:

4、更新AQS中的state值并返回:

而为啥这个state减1之后可能不为0呢?因为它是可重入的锁,回忆一下“可重入锁”的概念:

再来看AbstractQueuedSynchronizer.release():

接下来再回到主流程上往下分析。

条件一:已经释放:

其中看到Node了是不,其实也就是AQS中的等待队列,它的添加就是等待进行获取锁的线程,回忆一下添加到队列的逻辑:

其中具体看一下这块的细节:

然后调用:

跟进去瞅瞅。

unparkSuccessor():

关于这个遍历细节就略过了,就是链表的操作,接下来就开始要唤醒该线程了:

跟进去:

再往里跟:

又回到了底层实现了,此时要想进一步了解更加底层的逻辑就需要翻阅open jdk的代码了,关于open jdk在之前翻阅过一次,可以参考https://www.cnblogs.com/webor2006/p/11442551.html,这里就不往里跟了,不过它的实现具体在这个目录下:os/linux/vm/oslinux/cpp/packer.cpp,它里面就有一个unpark方法,而它底层都是使用了linux的库函数来实现的,典型的是使用了pthread_mutex_unlock来释放锁。可以看到,它跟synchronized差不多,虽说我们调用都是用的java代码,底层其实都为cpp实现的。

条件二:未释放:

而如果不满足释放的条件,则直接返回false了:

ReentrantLock执行逻辑总结:

以上所分析的对于公平与非公平锁其实都满足,下面对其进行一个文字性的总结:

1、尝试获取对象的锁,如果获取不到(意味着已经有其他线程持有了锁,并且尚未释放),那么它就会进入到AQS的阻塞队列当中。

2、如果获取到,那么根据锁是公平锁还是非公平锁来进行不同的处理:
        a、如果是公平锁,那么线程会直接放置到AQS阻塞队列的末尾

        b、如果是非公平锁,那么线程会首先尝试进行CAS计算,如果成功,则直接获取到锁;如果失败,则与公平锁的处理方式一致,被放到阻塞队列末尾。

3、当锁被释放时(调用了unlock方法),那么底层会调用release方法对state成员变量值进行减一操作,如果减一后,state值不为0,那么release操作就执行完毕;如果减一操作后,state值为0,则调用LockSupport的unpark方法唤醒该线程后的等待队列中的第一个后继线程(pthread_mutex_unlock),将其唤醒,使之能够获取到对象的锁(release时,对于公平锁与非公平锁的处理逻辑是一致的);之所以调用release方法后state值可能不为零,原因在于ReentrantLock是可重入锁,表示线程可以多次调用lock方法,导致每调用一次,state值都会加一。

最后思考一个问题:虽然我们说调用ReentrantLock的lock()方法就代表给线程上锁了,那这个上锁体现在哪里?回忆一下对于synchronized关键字上锁它的体现就是底层会以对象头来标识,也就是ObjectMonitor,然而对于ReentrantLock来说,大部分代码都是java代码,而它的上锁本质上就是对AQS中的state成员变量的操作;对该成员变量+1表示上锁;对该成员变量-1,表示释放锁

ReentrantReadWriteLock初探:

而对于AQS的另一个可重入的锁就如标题所示:

接下来先来对它进行一个基本的使用,有一个直观的认识:

读锁:

接下来先来看一下读锁的应用:

其中可以看到其使用方式跟ReentrantLock差不多,下面来调用一下:

其中还记得标箭头的这是啥语法么?java8中的方法引用,它是创建函数式接口实例的方式之一,其中Lambda表达式也是,这块不熟悉的可以参考https://www.cnblogs.com/webor2006/p/8135873.html,下面运行一下:

看到现象木有,多个线程并发执行了,毫无等待感,好,接下来下面来对比一下写锁的效果。

写锁:

修改一下代码:

再运行,此时记得跟上面的读锁的运行效果进行一个对比,基本上对于读取锁就能有一个非常好的认识,下面运行看一下:

也就是对于写锁它是一个排它模式的,当有一个线程持有了写锁,其它线程就得等待了, 而实际中往往是读多写少,也就是大多数的场景都是来读,而只有少部分是需要写的,很显然对于读操作是不需要加锁的,因为没有线程安全的问题对吧,而很显然使用这个读取锁就能够大大的增加程序的运行效率,而不像ReentrantLock,不管读还是写都是独占式的,那对于ReentrantReadWriteLock它底层对于AQS到底是怎么实现的呢?下次再来探究。

关注个人公众号,获得实时推送

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

webor2006

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

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

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

打赏作者

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

抵扣说明:

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

余额充值