ReentrantLock重入锁 源码解析1(公平锁加锁)

前言 

我们都知道在 jdk 1.6 之前 synchronized 是实打实的重量级锁, 所以在推出RenntraLock因其独特的机制,性能远远大于synchroized 但是作为亲儿子 synchronized 在 jdk 1.6 之后也借鉴了RenntraLock 的部分思路使其性能大大提高

笔者废话颇多,请读者通过目录拣选自己需要的部分阅读, 其中块引用区为一些小思维拓展可以直接跳过。

接下来便从源代码入手大致看一下总体的流程

下图为笔者自己总结的大致流程,仅供参考一切以源码为准

 

可能情况:

因其特殊性我们以几种情况进行讲解

1. 当前只有一个线程,并且在很长一段时间都只有这一个线程参与竞争

2. 有多个线程,他们以交替进行的方式(就是指正好在你释放锁的时候我来获取)

3.有多个线程,他们一起竞争

先明确两个概念 : 并发和并行

Q : 讲解方式中的第二点提到的交替进行是并发还是并行

A :他是并发而不是并行

Q:讲解方式中的第三点提到的一起竞争又是什么

A:他既是并发也是并行

如何理解并发和并行,我们可以从宏观的角度看待并发,从微观角度看待并行。

对于某个事件(比如炉石传说国服回归,各位玩家一起点击完成成就)在宏观角度上大家都在实现点击这件事,这就是并发。而在这个宏观的并发下面有可能因为网速等因素使得所有玩家都正好交替(你释放的时候我正好获取)完成了请求,使得倒霉的雷火服务器没有崩掉,可喜可贺,可喜可贺(当然我这么说只是方便理解,事实上完全不可能这样)这就不算是并行,而另一种情况玩家正好一起获取(一起抢锁)结果就把服务器挤爆了,可悲可悲,这就是并行。

通过这种现象,我们也可以得出怎样才能提高性能: 那就是尽可能在软件层面解决高并发问题

好的废话少说开始走一遍源码(部分不是很核心得部分就不讲了,大家可以自己看)

加锁!

这个 arg:1 具体是什么需要后面提到AQS 以后才方便说明,目前不会影响理解,只需直到这段代码的意思是说我想要拿到锁

这里分为两步

1. 能否直接获取锁

2.若不能直接获取则去看是否要开始创建一个排队队列,或者如果已经有队列了就跟着排在后面

tryAcquire

如图是公平锁的 tryAcquire 实现

getState 获取锁状态函数 1 为 锁已经被使用中,0为锁空闲。

这里我们先不看代码,自己想想如果你是开发者你要怎么设计 :

如果空闲的话就通过常规的双重校验机制拿锁, 如果没空闲的话就弄个队列在后面排队?

 这样还不够,前面说过了优化性能从软件层面入手也就是说希望我们能够尽可能权衡直接入队阻塞的性能损耗以及软件层面反复询问锁状况的损耗,权衡利弊之后找到最优的情况并执行。

天马行空的想一想最优解是什么?(也欢迎小伙伴在评论区提出你的看法)

笔者认为如果能精准预测出轮询的时间其实就可以找到最优解了,那么以后能否通过深度学习学习并发量和其他特征(比如复杂度等)尽可能找到优解。 我们看看现在的synchroized升级锁机制也是在想办法在软件层面解决问题,当然笔者才疏学浅,也只能当乐趣谈谈这种事。

1. 如果当前锁状态是 0 ,就进行双重校验(假如这个时候100个线程同时进入到这个位置,而实际上某些线程执行的比较快,连AQS 队列都生成并且大家都开始排队了)如果队列生成并已经排了几个人了那就返回 true 否则如果队列为空或者自己就是队列的头节点那就返回 false (具体情况涉及AQS 我们一会在讲,这里先把大框都说明白)

有关这里的双重校验如果没能立即理解可以看看下面的例子:

eg : 你是个大学牲大清早上去食堂买粥,食堂同时开了8个门,你跟8个同学同时进了门,但是周围一堆柱子,你就以为你是第一个进来的根本不着急,于是打开国服炉石沉浸在 >15 分钟 和对雷火🐎的亲切问候中,等你一抬头一看,牙白~ 队伍已经排的老长了

这里是进行了双重校验操作的情况,那如果没进行这个操作又会如何?

等你抬头的时候你完全不管排没排队,就直接去卖粥区跟阿姨买粥后面排队的人不打你么。

其实这里也能引发我们对于高并发的思考---正如这食堂一样我们往往大量设置的入口数量,可粥铺就一个就总有一天把食堂撑爆了

有什么思路做优化?

1. 当然最好的方法就是多开粥铺,但这种方式要考虑如果食堂生意不好靠着偶尔生意爆火那天的销售额能否将常态冷清的亏损(清洗费,保养费啊)挣回来,人力费又值不值得,但这个方法其实就很考验这个食堂经理的能力了(增加生产者,权衡闲置损耗)。

2.门口来个保安由他来界定是不是食堂人太多暂时不让外面的人进来(转移压力,将受压可能造成损失的地方的压力转给无所谓受压多少的地方)

3.扩张食堂!有钱任性(为啥他不是最好的方法,它的成本比1差太多了)

如果上面返回了true tryAcquire 直接返回false 

如果上面返回false 则进行CAS 操作将锁状态改为1,若成功(双重校验的一部分)将持有锁线程标注为当前线程

多线程问题之所以能够解决,其本质在于它最底下必须有一个(硬件上的原子性操作 CAS 就是一个原子性操作但是不具备可见性,具体不延申了。 因此如果能够提高这个最底下的原子性操作性能才是一劳永逸的解法,咳咳这里也就是java的性能没有那几位好的原因了,底层还是慢一些嘛。

并且返回 true

2.如果当前锁状态为1,则判断当前想要获取锁的线程是不是已经持有锁的线程(我抢我自己?真的假的)如果是真的 锁状态就加一(这种当然都是原子操作后面就不再注释了你知道就好)重入锁重入的体现,并返回true

tryAcquire() 返回true 表示它成功拿到锁了那就可以了 

依次类推,如果一直是我释放锁,你去拿,你释放后别人拿,那就会一直重复这个过程,这样的效率是很不错的,但通常情况都是一堆人一起抢 

入队!

acquireQueued

现在可以介绍一下AQS以及里面的 Node 了

thread 存放当前的线程 

prev 存放前置节点

next 存放后置节点

mode 模式感兴趣的可以自行了解

waitStatus 一种标识,后面详细介绍

而AQS 就是这个由Node 组成的阻塞队列

初始化状态:

出队状态:

出队即意味着当前Node 的线程获取到锁,因此  head = node.next

 

入队状态:

waitStatus 即表示他后面存在节点,这个标识的作用可以简单理解成 -1 状态就是迟早需要释放锁,而如果为0 那就表示当前就这一个线程对这个锁有需求那就一直拿着得了反正没人跟他要

切记,一定不要混淆释放锁和出队这两个概念, 出队是持有锁得开始,释放锁是持有锁得结束,是唤醒后续节点线程的开始

addWaiter(Node.EXCLUSIVE)

这里用个for(;;) 语句要求它一定要返回一个node 

我们再回来看

acquireQueued 传入addWaiter 返回的tail 结点

首先获取当前节点的前一个节点P,如果P 是头节点意味着这个P 获取了锁正在使用资源, 那么这里就通过tryAcquire函数不断询问锁释没释放释放了就进行出队操作

(这里也就体现在软件层面尝试解决问题,可在无法预知时间的情况下如果所有线程都在询问前面的节点反而不好了)所以如果这个P不是头节点,就给前置节点的waiteState设置为-1 (至于为什么这么做上文在“入队状态”提到过)同时阻塞当前线程,同时将interupted 设置为true 。

等到它的前置节点拿到锁的时候就会给他解锁。

解释一下入队这块的操作:

这里可以类比你在排队,如果你是队伍中排在第二个的那个人你当然有资格问问前面的哥们:哥们你弄没弄完,弄完换我呗。 但你要是排在第三个的人你有必要问你前面那个人:你前面的哥们弄完了嘛?完全没必要,老老实实玩手机去就好了

以上就是重入公平锁的加锁过程了,至于解锁过程比较简单,可以根据我给的流程图自己走一遍试试看。强烈建议自己跟着代码走的时候自己也写一份流程图出来加深理解

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

小户爱

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

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

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

打赏作者

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

抵扣说明:

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

余额充值