目录
ReentrantLock源码
原理
我们分析源码的时候最好先弄清楚背后的原理、规范
代码无非就是这些原理、规范的实现
我们来思考下加锁的场景下各种角色
- 资源
- 多个请求线程
- 获取资源失败的等待队列
在这个执行过程中对各个锁实现细节不同
比如
独享锁每次只有一个线程获取锁、共享锁多个线程获取锁
唤醒等待、唤醒一个线程、唤醒所有线程
所以对这个过程进行建模也就是管程模型
Java 管程模型
java使用的管程模型如下图
这幅图来自 王宝令-《Java并发编程》
Java使用的管程模型是 简化了MESA 管程模型,三个方法其实就是MESA中的规范
对应Java实现就是:synchronized 关键字和 wait()、notify()、notifyAll() 方法
这里也许会有疑问,synchronized 关键字已经解决并发问题了,那条件变量和三个条件是用来做什么的?
在并发变成过程中,我们遇到问题往往不仅仅是多线程间的互斥(抢锁)这一个场景
有的时候一个线程虽然抢到了锁,但是执行业务的条件还没有准备好,这个时候单纯的释放锁重新竞争往往是无效的
注意:这里的无效意思是说,线程有可能立马有抢占到锁,这个时候条件还是不成立,其实是浪费了CPU的执行时间
那就需要有一个通知唤醒的机制:也就是线程间通讯
具体步骤:
- 线程发现无法继续执行就使用条件变量A.await()释放锁并进入休眠
- 另外的线程发现条件满足了就使用条件变量A.notifyAll()去唤醒条件变量A休眠的线程
- 让休眠的线程重新进入等待队列
我们知道 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互斥流程