005-ReentrantLock原理和源码解析

ReentrantLock源码

原理

我们分析源码的时候最好先弄清楚背后的原理、规范
代码无非就是这些原理、规范的实现

我们来思考下加锁的场景下各种角色

  • 资源
  • 多个请求线程
  • 获取资源失败的等待队列
    在这里插入图片描述
    在这个执行过程中对各个锁实现细节不同
    比如
    独享锁每次只有一个线程获取锁、共享锁多个线程获取锁
    唤醒等待、唤醒一个线程、唤醒所有线程
    所以对这个过程进行建模也就是管程模型

Java 管程模型

java使用的管程模型如下图
在这里插入图片描述
这幅图来自 王宝令-《Java并发编程》
Java使用的管程模型是 简化了MESA 管程模型,三个方法其实就是MESA中的规范
对应Java实现就是:synchronized 关键字和 wait()、notify()、notifyAll() 方法

这里也许会有疑问,synchronized 关键字已经解决并发问题了,那条件变量和三个条件是用来做什么的?
在并发变成过程中,我们遇到问题往往不仅仅是多线程间的互斥(抢锁)这一个场景
有的时候一个线程虽然抢到了锁,但是执行业务的条件还没有准备好,这个时候单纯的释放锁重新竞争往往是无效的
注意:这里的无效意思是说,线程有可能立马有抢占到锁,这个时候条件还是不成立,其实是浪费了CPU的执行时间
那就需要有一个通知唤醒的机制:也就是线程间通讯
具体步骤:

  1. 线程发现无法继续执行就使用条件变量A.await()释放锁并进入休眠
  2. 另外的线程发现条件满足了就使用条件变量A.notifyAll()去唤醒条件变量A休眠的线程
  3. 让休眠的线程重新进入等待队列

我们知道 synchronized 关键字有的时候使用不是很方便,也无法扩展:如修改为不可重入锁
所以Java就提供了管程的更加底层的实现模板,让我们能控制管程中各个部分的实现
也就是AQS(AbstractQueuedSynchronizer)
直译来说就是:抽象队列同步器
之所以不是接口而是抽象类,是因为AQS已经实现了对进入入口等待队列等操作

那我们现在来分析下AQS
我们先找到常用的ReentrantLock
查看代码会发现是 基于AQS state实现的
在这里插入图片描述
那AQS state是什么?

AQS state

我们来回顾管程模型
其实 state 就是模型中的共享变量V:用来表示锁的状态
当一个线程试图获取锁时,它会尝试更新state的值。
如果更新成功,表明线程获得了锁;
如果更新失败,则表明锁已被其他线程持有,当前线程需要排队等待。
AQS已经实现了获取锁失败后的入队处理
实现类需要做的就是实现对锁的获取(acquire)和释放(release)

AbstractQueuedSynchronizer

在这里插入图片描述
注释里说了如果要基于AbstractQueuedSynchronizer实现同步器
则需要重写对 state 字段的操作、获取方法
对应的就是
获取:tryAcquire、TryAcquireShared
释放:tryRelease、TryReleaseShared
判断是否独占:isHeldExclusively

Acquire 获取

获取成功则返回true、失败则返回false

boolean tryAcquire(int arg)

共享模式、获取失败则返回 <0的数字、成功且允许继续请求获取则返回>0的数字、成功且不允许继续请求则返回0

int tryAcquireShared(int arg)

Release 释放

完全释放,其他等待线程可以尝试获取返回true,否则返回false
必须由持有锁的线程调用

boolean tryRelease(int arg)
共享模式下使用:boolean tryReleaseShared(int arg)

大致需要实现的接口我们了解了
但是在分析源码的过程中最好带着目的去分析
可以尝试分析如下问题

  • 公平锁和非公平锁怎么实现
  • 可重入锁是怎么实现

ReentrantLock源码

构造器
在这里插入图片描述
就是默认是非公平锁
在这里插入图片描述
所有线程执行lock()都进行尝试加锁(cas state 从 0 到 1)
如果成功则设置独占线程 OwnerThread
如果失败则执行acquire(1)

因为所有的线程都先尝试加锁,所以后执行的线程有可能获取锁:非公平
同理公平锁的lock()实现如下
在这里插入图片描述

acquire(1)是干嘛的?注意在非公平锁只有没有拿到锁的线程会走这里

源码-未获取锁线程-lock()

那么接下来的分析都是没有拿到锁的线程,假设线程名称为:线程-02
在这里插入图片描述
调用实现类的tryAcquire 这里肯定返回false 取反为 true
然后执行 acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
先执行 addWaiter(Node.EXCLUSIVE) 也就是 addWaiter(null)
在这里插入图片描述
因为tail也是null,所以直接执行 enq(node(线程-02));

在这里插入图片描述
这个就是AQS帮我们写好的入队操作
循环 {
如果尾部是null
则 cas head从 null 到 一个new Node(我们叫头节点)且尾部节点 = 头节点
失败或执行结束则继续循环
如果尾部不是null
则入参node.prev = 尾节点
cas 尾部节点从尾节点到入参node
成功则前尾节点.next = 入参node
}
也就是
第一次循环创建了一个双线链表
在这里插入图片描述
第二次循环把当前线程设置到链表尾部
在这里插入图片描述

addWaiter执行完了该执行 acquireQueued(node(线程-02), 1))
在这里插入图片描述
红框中的逻辑为
predecessor()获取前置node:p
如果前置节点p为 head 并且tryAcquire : 这里肯定失败,因为我们分析的是获取锁失败的线程
shouldParkAfterFailedAcquire(前置节点, node(线程-02))
在这里插入图片描述
因为前置节点的waitStatus=0 则cas waitStatus 从 0 到 -1
然后返回false
重新进入红框里的循环,这次前置节点.waitStatus = -1 则直接返回true
开始执行parkAndCheckInterrupt()
在这里插入图片描述

这里直接park当前线程,也就是:线程-02,线程-02此时wait了让出cpu,就是卡在红框这里
代码流程是

acquire(1) > acquireQueued > parkAndCheckInterrupt

此时队列
在这里插入图片描述

源码-获取锁线程-重入lock()

现在再让我们分析下获取锁线程的重入lock()
在这里插入图片描述
这里即使是获取锁线程cas也会失败
进入acquire(1) 从前面我们直到acquire第一步是执行tryAcquire,也就是执行非公平锁的方法
在这里插入图片描述
底层就调用的这个方法入参为1
getState()返回为1,进入else if nextc=1+1=2
然后setState(nextc)
也就是说重入几次state就累加几次1
这个是为了之后判断是否完全释放锁做准备

源码-获取锁线程-unlock()

一般来说不会有没有获取锁的线程调用unlock()
会进去AQS的release(1)
在这里插入图片描述
这里因为是获取锁的线程,那么tryRelease(1)是会成功的
获取headNode
如果headNode不为空且waitStatus不为0,也就是说有线程因为枪锁进入等待了
那么就唤醒接班人unparkSuccessor(headNode)
在这里插入图片描述
wa=-1 执行 CAS waitStatus 从 -1 到 0
获取nextNode,也就是node(线程-02)
不为null,则执行node(线程-02) 的 unpark
然后返回true

而tryRelease(1)中就是把state -1 并设置给 state 这里不用cas了因为没有并发
如果state==0 了就设置独占线程为null
在这里插入图片描述

源码-没有获取锁并进入等待线程-被唤醒后

线程等待的地方
在这里插入图片描述
线程等待的调用链

acquire(1) > acquireQueued > parkAndCheckInterrupt

parkAndCheckInterrupt因为是正常唤醒所以返回false

重新进入 acquireQueued

进入无限循环
在这里插入图片描述
获取node(线程-02)的前置节点设为p
如果p是头节点并且 tryAcquire(1)返回true
从这里看出来,进入入口等待队列的线程只能依次获取锁,因为其他线程的前置不是headNode
把当前节点设置为head
p的next为null
返回false

之前分析过tryAcquire(1)中
CAS state 从 0 到 1
因为没有锁了
所以成功了
设置独占线程为当前线程

重新进入 acquire(1)

没有需要处理的直接跳出

队列的变化就是
从这个
在这里插入图片描述
变更成这个
在这里插入图片描述

总结

ReentrantLocal互斥流程
在这里插入图片描述

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值