ReentrantLock 的公平锁源码分析
ReentrantLock 源码分析 以公平锁源码解析为例:
1:数据结构:
维护Sync 对象的引用: private final Sync sync;
Sync对象继承 AQS, Sync 分为两个类:处理公平锁锁和非公平锁:
FairSync NonfairSync
具体的类图如下:

2:接下来重点分析AQS这个类:AbstractQueuedSynchronizer:
AQS中的成员变量:
private transient volatile Node head; //AQS维护队列的头结点
private transient volatile Node tail; // AQS维护队列的尾结点
private volatile int state; // AQS 锁的状态 数量标识锁被获取的次数
下面看看NODE 结点的成员变量:
volatile int waitStatus; //等待状态
volatile Node prev; //前继节点
volatile Node next; //后继节点
volatile Thread thread; //线程对象
Node nextWaiter; //下一个等待节点
从NODE的数据结构可以看出来,AQS里面维护的队列的数据结构是双链表的形式;

3:接下来分析 ReentrantLock 的构造方法:
ReentrantLock lock = new ReentrantLock(true); //传入true,说明是构造公平锁
具体的构造方法如下,返回FairSync 对象:
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
4:lock方法的分析:因为是公平锁,所以调用 FairSync 下的lock方法:
final void lock() {
acquire(1);
}
Acquire 的方法如下:
public final void acquire(int arg) { // arg=1
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
接下来分两种场景来分析tryAcquire(arg) 1:同一线程第一次或N次获取锁(其他线程没有获取到锁) 2:其他线程已经获取到锁,当前线程尝试去获取锁;
//场景 1:
protected final boolean tryAcquire(int acquires) { // acquires=1
final Thread current = Thread.currentThread(); //当前线程:main-thread
int c = getState(); //如果为第一次获取锁c=0 如果main线程已经获取过锁,则c为加锁的次数
if (c == 0) { //当前线程第一次获取锁
if (!hasQueuedPredecessors() && // hasQueuedPredecessor的分析如下单独分析:
compareAndSetState(0, acquires)) { //cas原子操作 state=1
setExclusiveOwnerThread(current); //独占线程设置为当前线程
return true; //返回true表明加锁成功
}
}
else if (current == getExclusiveOwnerThread()) { // c=n 的情况下
int nextc = c + acquires; // nextc=n+1
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc); //设置 state=n+1
return true; //获取到锁,返回true
}
return false;
}
说明:hasQueuedPredecessors主要是判断当前线程所在的节点是不是CLH队列的首个位置,这个判断的目的是公平锁的公平获取锁的机制
hasQueuedPredecessors的源码如下:以该线程是第一次获取锁为例分析:
public final boolean hasQueuedPredecessors() {
Node t = tail; // tail=null
Node h = head; // head=null
Node s;
return h != t && //返回false
((s = h.next) == null || s.thread != Thread.currentThread());
}
//场景2 分析:有其他线程未释放锁(main-thread 持有锁,thread-1尝试去获取锁)
protected final boolean tryAcquire(int acquires) { // acquires=1
final Thread current = Thread.currentThread(); //当前线程:thread-1
int c = getState(); // 由于其他持有锁 state 至少为1
if (c == 0) {
if(!hasQueuedPredecessors()&& compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) { // current=thread1 // getExclusiveOwnerThread()=main
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false; //此时返回false表明尝试获取锁失败
}
5:上面的场景1 获取到锁后返回true,则lock 方法执行结束。下面分析场景2:
public final void acquire(int arg) {
if (!tryAcquire(arg) && //尝试获取锁失败,返回false
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
!tryAcquire(arg) 返回为true;接下来进入
acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) 这个逻辑;
这个方法有两层处理:
1:addWaiter(Node.EXCLUSIVE) 将当前线程调价到CLH队列中
2:acquireQueued();逐步执行CLH队列中的线程,如果当前线程获取到锁则返回,否则,当前线程进行休眠,直到唤醒并重新获取到锁才返回;
以下分析这两个方法:场景:main线程获取到锁但未释放,这是线程 thread-1去获取锁:(假设此时没有其他线程在CLH队列中,即CLH队列为null)
addWaiter(Node.EXCLUSIVE)方法:入参 Node.EXCLUSIVE 为null;
private Node addWaiter(Node mode) { //mode=null EXCLUSIVE 标识节点为独占锁模型
Node node = new Node(Thread.currentThread(), mode); //创建新节点:新节点中线程为当前线程,节点模型为独占锁:thread= thread-1 nextWaiter= mode
// Try the fast path of enq; backup to full enq on failure
Node pred = tail; // 此时tail=null
if (pred != null) {
node.prev = pred;
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
enq(node); //进入enq方法
return node;
}
Enq() 方法如下:
private Node enq(final Node node) { //入参为上一步新建的节点
for (;;) {
Node t = tail; // 第一次遍历逻辑 tail=null
if (t == null) { // Must initialize
if (compareAndSetHead(new Node())) //CAS 创建表头 Head
tail = head;
} else {
node.prev = t; //第二次遍历逻辑:node为上面新建的节点:thread= thread-1 nextWaiter= mode, node.prev指向表头 t 为表头
if (compareAndSetTail(t, node)) { //CAS设置队列尾节点为当前node
t.next = node; // 表头后继节点指向当前节点
return t;
}
}
}
}
该场景经过上面的处理之后 CLH队列的数据结构如下:
第一次遍历:
Head,tail节点
第二次遍历:
Head 当前线程node:设置为tail
接下来分析
acquireQueued这个方法
final boolean acquireQueued(final Node node, int arg) { //node为当前线程节点 arg=1
boolean failed = true;
try {
boolean interrupted = false; //当前线程在休眠时,有没有被中断过
for (;;) {
final Node p = node.predecessor(); //获取前继节点,这里为head节点
if (p == head && tryAcquire(arg)) { //这里p== head为true,接下来进入
// tryAcquire(arg)这个方法,tryAcquire(arg)方法前面分析过了,这里返回false;
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
// 接下来会进入以下的逻辑:下面会分析这两个方法
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
这里再次回顾下tryAcquire(arg) 方法:返回fasle
protected final boolean tryAcquire(int acquires) { // acquires=1
final Thread current = Thread.currentThread(); //当前线程 thread-1
int c = getState(); //state=1
if (c == 0) {
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) { // getExclusiveOwnerThread
获取到的线程是tryAcquire(int acquires)中设置的值 这里是 main; current=thread-1
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
}
1): acquireQueued(final Node node, int arg) 方法中for循环第一次执行shouldParkAfterFailedAcquire(p, node) 方法分析:源码如下:
入参:pred为前继节点,这里是head node为当前节点
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus; // ws=0
if (ws == Node.SIGNAL)
return true;
if (ws > 0) {
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
compareAndSetWaitStatus(pred, ws, Node.SIGNAL); //设置前继节点waitStatus= Node.SIGNAL,
}
return false; //返回false;
}
For循环第二次执行 shouldParkAfterFailedAcquire(p, node)方法:
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus; // ws=-1 在第一次已经设置为-1
if (ws == Node.SIGNAL) //返回true
return true;
if (ws > 0) {
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
compareAndSetWaitStatus(pred, ws, Node.SIGNAL); //设置前继节点waitStatus= Node.SIGNAL,
}
return false; //返回false;
}
返回true后,接下进入parkAndCheckInterrupt()这个方法:
源码分析如下:
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this); //阻塞当前线程
return Thread.interrupted(); // 返回线程的中断状态
}
LockSupport.park(this); //作用:前继线程节点的状态是 Node.SIGNAL;挂起当前线程;
Thread.interrupted(); //当前被挂起的线程被前继线程中断,返回线程的中断状态;
下面解释一个线程的行为:LockSupport.park(this) 线程被挂起:
当线程被挂起的时候唤醒的方式有两种:
1:unpark 的方式唤醒,前继节点线程使用完锁后,通过unpark方式唤醒当前线程
2:中断唤醒,其他线程通过 interrupt 中断当前线程
接下来继续分析:acquire(int arg)
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
这个时候的重点放在分析 selfInterrupt(); 这个方法上;
进入这个方法的条件是 当前线程被中断过,并且获取锁成功了;
static void selfInterrupt() {
Thread.currentThread().interrupt(); //当前线程产生一个中断,真正被唤醒
}
到此为止,ReentrantLock 的公平锁源码分析结束。
ReentrantLock 的公平锁源码分析的更多相关文章
- ReentrantLock之公平锁源码分析
本文分析的ReentrantLock所对应的Java版本为JDK8. 在阅读本文前,读者应该知道什么是CAS.自旋. 本文大纲 1.ReentrantLock公平锁简介 2.AQS 3.lock方法 ...
- ReentrantLock之非公平锁源码分析
本文分析的ReentrantLock所对应的Java版本为JDK8. 在阅读本文前,读者应该知道什么是CAS.自旋. 由于ReentrantLock的公平锁和非公平锁中有许多共同代码,本文只会对这两种 ...
- (转)ReentrantLock实现原理及源码分析
背景:ReetrantLock底层是基于AQS实现的(CAS+CHL),有公平和非公平两种区别. 这种底层机制,很有必要通过跟踪源码来进行分析. 参考 ReentrantLock实现原理及源码分析 源 ...
- Redisson分布式锁学习总结:可重入锁 RedissonLock#lock 获取锁源码分析
原文:Redisson分布式锁学习总结:可重入锁 RedissonLock#lock 获取锁源码分析 一.RedissonLock#lock 源码分析 1.根据锁key计算出 slot,一个slot对 ...
- ReentrantLock 公平锁源码 第0篇
ReentrantLock 0 关于ReentrantLock的文章其实写过的,但当时写的感觉不是太好,就给删了,那为啥又要再写一遍呢 最近闲着没事想自己写个锁,然后整了几天出来后不是跑丢线程就是和没 ...
- ReentrantLock 公平锁源码 第1篇
ReentrantLock 1 这篇还是接着ReentrantLock的公平锁,没看过第0篇的可以先去看上一篇https://www.cnblogs.com/sunankang/p/16456342. ...
- ReentrantLock实现原理及源码分析
ReentrantLock是Java并发包中提供的一个可重入的互斥锁.ReentrantLock和synchronized在基本用法,行为语义上都是类似的,同样都具有可重入性.只不过相比原生的Sync ...
- 基于ReentrantLock的AQS的源码分析(独占、非中断、不超时部分)
刚刚看完了并发实践这本书,算是理论具备了,看到了AQS的介绍,再看看源码,发现要想把并发理解透还是很难得,花了几个小时细分析了一下把可能出现的场景尽可能的往代码中去套,还是有些收获,但是真的很费脑,还 ...
- RedissonLock分布式锁源码分析
最近碰到的一个问题,Java代码中写了一个定时器,分布式部署的时候,多台同时执行的话就会出现重复的数据,为了避免这种情况,之前是通过在配置文件里写上可以执行这段代码的IP,代码中判断如果跟这个IP相等 ...
随机推荐
- vue chunk-elementUI.3d5a4739.js 过大,网页打开慢开启gzip压缩
如下 upstream sems { server weight= fail_timeout=; } server { listen ; server_name www.serve.com; loca ...
- v-bind 属性绑定
1.v-bind:title="title" 绑定谁和谁绑定. 2.v-bind:title="title" 简写::title="title&quo ...
- 批量操作mysql数据库表
SELECT CONCAT('truncate TABLE ',table_schema,'.',TABLE_NAME, ';') FROM INFORMATION_SCHEMA.TABLES WHE ...
- 在eclipse中使用Maven分模块搭建SSM框架,创建jar、war、pom工程模块教学,项目的热部署,需要的可以留下邮箱,给大家发整理过的Maven笔记
第一章:Maven概述 (1)Maven是一个项目构建工具,由apache提供,用Java开发的 (2)构建工具:Ant(蚂蚁),Maven(专家) (3)构建工具作用:帮助程序员进行项目的创建,目录 ...
- IoU与非极大值抑制(NMS)的理解与实现
1. IoU(区域交并比) 计算IoU的公式如下图,可以看到IoU是一个比值,即交并比. 在分子中,我们计算预测框和ground-truth之间的重叠区域: 分母是并集区域,或者更简单地说,是预测框和 ...
- C++ 梳理:跑通简单程序
C++ 结合了三个编程流派: 过程式编程:C 特性,结构化语言,强调过程 面向对象编程:C++ 对于 C 语言的补充,一切皆对象,强调数据 泛型编程(generic programming):由 C+ ...
- 使用istioctl命令查看gateway及virtualservices
istioctl命令,比kubectl命令,在查看istio资源方面,要方便很多. 如果使用microk8s安装,则命令为microk8s.istioctl了. 查看gateway及virtualse ...
- C++小工具
1.Doxygen 从源代码生成文档.可以生成在线文档(HTML)和离线手册(以LATEX格式),还可以自动生成各种依赖关系图,继承关系图等.
- 201871010115——马北《面向对象程序设计JAVA》第二周学习总结
项目 内容 这个作业属于哪个课程 https://www.cnblogs.com/nwnu-daizh/ 这个作业的要求在哪里 https://www.cnblogs.com/nwnu-daizh/p ...
- 五个goland进行go开发的小技巧
五个goland进行go开发的小技巧 本文译自5 Tips To Speed Up Golang Development With IntelliJ Or Goland 确实很实用. 1. 实现int ...