重点,一文掌握ReentrantLock加解锁原理!|原创

news/2024/5/2 19:22:14/文章来源:https://blog.csdn.net/sinat_32873711/article/details/128030617

本文详细讲解了 ReentrantLock 加锁和释放锁的原理,以及和 Synchronized 的对比。本文较长,建议收藏!

点击上方“后端开发技术”,选择“设为星标” ,优质资源及时送达

简要总结 ReentrantLock

实现原理:volatile 变量 + CAS设置值 + AQS + 两个队列

实现阻塞:同步队列 + CAS抢占标记为 valatile 的 state

实现等待唤醒:await :持有锁,park ->加入等待队列 ;signal:唤醒下一个等待队列节点,转移进入同步队列,然后CAS抢占或者按照阻塞队列等待抢占。接着 await 后续内容程序得以继续执行。

ReentrantLock 结构分析

ReentrantLock 继承了Lock接口, lock方法实际上是调用了Sync的子类NonfairSync(非公平锁)的lock方法。ReentrantLock的真正实现在他的两个内部类NonfairSync 和 FairSync中,默认实现是非公平锁。并且内部类都继承于内部类Sync,而Sync根本的实现则是大名鼎鼎的 AbstractQueuedSynchronizer 同步器(AQS)。

ace281765102e06b86ef3b69d4834175.png

具体详见如下代码:

public class ReentrantLock implements Lock, java.io.Serializable {private static final long serialVersionUID = 7373984872572414699L;/** Synchronizer providing all implementation mechanics */private final Sync sync;public ReentrantLock() {sync = new NonfairSync();}abstract static class Sync extends AbstractQueuedSynchronizer {……省略代码}//非公平锁static final class NonfairSync extends Sync {private static final long serialVersionUID = 7316153563782823691L;/*** Performs lock.  Try immediate barge, backing up to normal* acquire on failure.*/final void lock() {if (compareAndSetState(0, 1))setExclusiveOwnerThread(Thread.currentThread());elseacquire(1);}protected final boolean tryAcquire(int acquires) {return nonfairTryAcquire(acquires);}}// 公平锁static final class FairSync extends Sync {private static final long serialVersionUID = -3000897897090466540L;final void lock() {acquire(1);}/*** Fair version of tryAcquire.  Don't grant access unless* recursive call or no waiters or is first.*/protected final boolean tryAcquire(int acquires) {
……省略}}// lock 方法本质就是调用sync类public void lock() {sync.lock();}
}

lock 加锁过程

按照调用 lock 方法是否抢占锁成功,可以以调用 park 方法为界限,将加锁的过程分为两部分:一部分是当前线程被阻塞前,另一部分是线程被唤醒继续执行后。(这里以非公平锁为例)

阻塞前

1.直接通过CAS尝试获取锁,设置state为1。如果获取成功则将锁标识设为独占,就是是将当前线程设置给 exclusiveOwnerThread。

final void lock() {if (compareAndSetState(0, 1))setExclusiveOwnerThread(Thread.currentThread());elseacquire(1);
}

2.如果获取失败,再次尝试获取,调用acquire。

3.tryAcquire ->:判断锁是否被占有,如果空闲则再次尝试CAS获取锁;如果已被占有则对比占有锁的线程是否为本线程,是的话将state+1,这就是可重入锁的关键逻辑。

//AbstractQueuedSynchronizer
public final void acquire(int arg) {if (!tryAcquire(arg) &&acquireQueued(addWaiter(Node.EXCLUSIVE), arg))selfInterrupt();
}
//ReentrantLock.NonfairSync
protected final boolean tryAcquire(int acquires) {return nonfairTryAcquire(acquires);
}
//ReentrantLock.Sync
final boolean nonfairTryAcquire(int acquires) {final Thread current = Thread.currentThread();int c = getState();if (c == 0) {// cas再次尝试获取if (compareAndSetState(0, acquires)) {setExclusiveOwnerThread(current);return true;}}else if (current == getExclusiveOwnerThread()) {// 可重入逻辑int nextc = c + acquires;if (nextc < 0) // overflowthrow new Error("Maximum lock count exceeded");setState(nextc);return true;}return false;
}

4.如果获取失败则将节点插入队列尾部,如果队列为空,则会初始化队列,并且设置头尾节点为空节点,再将Node设为尾节点。

// 获取锁失败
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))// 加入同步队列
private Node addWaiter(Node mode) {Node node = new Node(Thread.currentThread(), mode);// Try the fast path of enq; backup to full enq on failureNode pred = tail;if (pred != null) {node.prev = pred;// 通过CAS设置尾节点为当前节点,前驱节点为之前的尾节点。if (compareAndSetTail(pred, node)) {pred.next = node;return node;}}// 如果当前链表为空,则在此处进行初始化enq(node);return node;
}
private Node enq(final Node node) {for (;;) {Node t = tail;if (t == null) { // Must initializeif (compareAndSetHead(new Node()))tail = head;} else {// 追加到队列尾node.prev = t;if (compareAndSetTail(t, node)) {t.next = node;return t;}}}
}

5.将新建的Node传入acquireQueued,获取前驱节点,如果节点就是head 头节点,那么尝试CAS竞争锁(head随时释放)。如果抢占成功将头节点设为自己。

final boolean acquireQueued(final Node node, int arg) {boolean failed = true;try {boolean interrupted = false;for (;;) {final Node p = node.predecessor();// 如果是头节点,再次尝试if (p == head && tryAcquire(arg)) {setHead(node);p.next = null; // help GCfailed = false;return interrupted;}if (shouldParkAfterFailedAcquire(p, node) &&parkAndCheckInterrupt())interrupted = true;}} finally {if (failed)cancelAcquire(node);}
}

6.如果没有抢占成功,则进入shouldParkAfterFailedAcquire逻辑,将前驱节点设置为Signal,表示后继节点(也就是当前节点)需要前驱节点去唤醒。设置完之后再次进入自旋锁,尝试获得锁。

关于Node的状态这里说明一下:

节点刚创建的时候,status=0,假设这时候本节点就是head节点,那么他会进入else逻辑,将自身状态设置为Signal,然后再次进入自旋,尝试获取锁。如果还是没有获取到锁,那么再次进入shouldParkAfterFailedAcquire方法后会进入第一个if逻辑,方法返回True。

/**
* Checks and updates status for a node that failed to acquire.
* Returns true if thread should block. This is the main signal
* control in all acquire loops.  Requires that pred == node.prev.
* 如果获取锁失败,检查并且更新节点。如果需要被park阻塞,返回true。
* 在所有的循环逻辑中,这是主要的信号控制逻辑。
*
* pred:表示前驱节点
* node:表示当前线程节点
*/
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {int ws = pred.waitStatus;if (ws == Node.SIGNAL)// 第二次尝试获取锁会进入这段逻辑/** This node has already set status asking a release* to signal it, so it can safely park.*/// 表明线程已经准备好被阻塞并等待之后被唤醒return true;if (ws > 0) {/** Predecessor was cancelled. Skip over predecessors and* indicate retry.*/// 若pred.waitStatus状态位大于0,说明这个前驱点已经取消了获取锁的操作,// doWhile循环会递归删除掉这些放弃获取锁的节点do {node.prev = pred = pred.prev;} while (pred.waitStatus > 0);pred.next = node;} else {/** 节点刚创建的时候,status=0,逻辑会走到这里将自身状态设置为signal* waitStatus must be 0 or PROPAGATE.  Indicate that we* need a signal, but don't park yet.  Caller will need to* retry to make sure it cannot acquire before parking.*///若状态位不为Node.SIGNAL,且没有取消操作,则会尝试将前驱节点状态位修改为Node.SIGNAL// 表示将会唤醒后继节点compareAndSetWaitStatus(pred, ws, Node.SIGNAL);}return false;
}

7.第二次自旋获取失败后,由于前驱节点已经是Signal,这时进入parkAndCheckInterrupt,将当前线程阻塞,等待被唤醒。后续其他线程如果也尝试抢占锁,会同样被阻塞。

private final boolean parkAndCheckInterrupt() {// 阻塞线程LockSupport.park(this);// 线程继续执行return Thread.interrupted();
}


park方法被唤醒后

在其他线程释放锁资源后,唤醒下一个节点,park的后半部分逻辑继续执行。

  1. 继续执行之前Park之后的逻辑,在此处线程被唤醒。这里会返回中断标记,这也是为什么ReentrantLock可以相应中断的原因。

947bba3358a922f1bc9d0b26dce803e8.png
  1. 然后再次进入自旋锁,使用CAS获取到锁标记,将头节点设为当前节点,然后返回中断标记跳出循环。

  2. 至此,获取锁流程结束。

unlock 释放锁过程

1.尝试释放锁,用state减去1,判断是否等于0。如果等于0表示已经完全释放锁,将线程标记设为null。否则释放失败,表示当前线程仍在继续持有,继续持有说明有重入情况。

// ReentrantLock
public void unlock() {sync.release(1);
}
// AQS
public final boolean release(int arg) {// 释放锁if (tryRelease(arg)) {Node h = head;if (h != null && h.waitStatus != 0)// 唤醒后继节点unparkSuccessor(h);return true;}return false;
}
// 释放锁
protected final boolean tryRelease(int releases) {int c = getState() - releases;if (Thread.currentThread() != getExclusiveOwnerThread())throw new IllegalMonitorStateException();boolean free = false;if (c == 0) {free = true;// 释放锁setExclusiveOwnerThread(null);}setState(c);return free;
}

2.拿到头节点,然后解锁后继节点。如果当前节点状态小于0(signal=-1),则修改节点status为0。然后向后递归找到status小于等于0的节点(正常为0),调用unpark解除阻塞。返回解锁成功。

// 唤醒后继节点
private void unparkSuccessor(Node node) {/** If status is negative (i.e., possibly needing signal) try* to clear in anticipation of signalling.  It is OK if this* fails or if status is changed by waiting thread.*/int ws = node.waitStatus;if (ws < 0)compareAndSetWaitStatus(node, ws, 0);/** Thread to unpark is held in successor, which is normally* just the next node.  But if cancelled or apparently null,* traverse backwards from tail to find the actual* non-cancelled successor.*/// 拿到下一个节点Node s = node.next;//要解除阻塞的线程在后继节点中,通常只是下一个节点。但如果取消或明显为空,则从尾部向前遍历以找到实际未取消的继任者。if (s == null || s.waitStatus > 0) {s = null;for (Node t = tail; t != null && t != node; t = t.prev)if (t.waitStatus <= 0)s = t;}if (s != null)//解锁LockSupport.unpark(s.thread);
}
cbb020a87ff4b9897e58151568550573.png

3.在这之后便继续开始执行之前被阻塞的线程中的逻辑。

到这里 ReentrantLock 的加解锁过程原理便讲解结束,关于条件队列的内容,有兴趣后续文章会做讲解。

对比 Synchronized

既然已经了解了 ReentrantLock ,那么在此对大家所熟知的 Synchronized 进行一个对比。

与Synchronized相同点:

1.ReentrantLock和synchronized都是独占锁,只允许线程互斥的访问临界区。

但是实现上两者不同:synchronized加锁解锁的过程是隐式的,用户不用手动操作,优点是操作简单,但显得不够灵活。一般并发场景使用synchronized的就够了;ReentrantLock需要手动加锁和解锁,且解锁的操作尽量要放在finally代码块中,保证线程正确释放锁。ReentrantLock操作较为复杂,但是因为可以手动控制加锁和解锁过程,在复杂的并发场景中能派上用场。

2.ReentrantLock和synchronized都是可重入锁。

synchronized因为可重入因此可以放在被递归执行的方法上,且不用担心线程最后能否正确释放锁;而ReentrantLock在重入时要却确保重复获取锁的次数必须和重复释放锁的次数一样,否则可能导致其他线程无法获得该锁。

3.都可以实现线程之间的等待通知机制。使用synchronized结合Object上的wait和notify方法可以实现线程间的等待通知机制。ReentrantLock结合Condition接口同样可以实现这个功能。而且相比前者使用起来更清晰也更简单。

与Synchronized 不同点:

  1. ReentrantLock是Java层面的实现,synchronized是JVM层面的实现。

  2. 使用synchronized关键字实现同步,线程执行完同步代码块会自动释放锁(a 线程执行完同步代码会释放锁 ;b 线程执行过程中发生异常会释放锁),而ReentrantLock需要手动释放锁需在finally中手工释放锁(unlock()方法释放锁),否则容易造成线程死锁。

  3. synchronized是非公平锁,ReentrantLock可以实现公平和非公平锁。

  4. ReentrantLock 可以设置超时获取锁。在指定的截止时间之前获取锁,如果截止时间到了还没有获取到锁,则返回。配合重试机制更好的解决死锁。

  5. ReentrantLock上等待获取锁的线程是可中断的,线程可以放弃等待锁。而synchonized会无限期等待下去。

  6. ReentrantLock 的 tryLock() 方法可以尝试非阻塞的获取锁,调用该方法后立刻返回,如果能够获取则返回true,否则返回false。

  7. synchronized无法判断是否获取锁的状态,Lock可以判断是否获取到锁,并且可以主动尝试去获取锁。

最后,欢迎大家提问和交流。

如果对你有帮助,欢迎点赞、评论或分享,感谢阅读!

update在MySQL中是怎样执行的,一张图牢记|原创

2022-11-19

8347b3ec709c87f58c96f4e5d2f65d66.jpeg

从二叉查找树到B*树,一文搞懂搜索树的演进!|原创

2022-11-14

410ed804bea17b122f9712a5ab84b1e0.jpeg

问到ThreadLocal,看这一篇就够了|原创

2022-10-13

6da4801fb6b9d1b47120e564dc9536f6.jpeg

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.luyixian.cn/news_show_225350.aspx

如若内容造成侵权/违法违规/事实不符,请联系dt猫网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Android入门第33天-Android里的弹出式对话框

简介 Android Studio里在4.0前有一种ProgressDialog&#xff0c;这个已经淘汰了。我们完全可以使用ProgressBar来取代。但是还有一种Dialog叫PopWindow&#xff0c;它是一种“可阻塞式Dialog”。即弹出后除非你给它一个“动作”否则就一直显示在那。 今天我们就来看看这种Dia…

【Linux】基础IO —— 动静态库的制作与使用

&#x1f308;欢迎来到Linux专栏~~动静态库的制作与使用 (꒪ꇴ꒪(꒪ꇴ꒪ )&#x1f423;,我是Scort目前状态&#xff1a;大三非科班啃C中&#x1f30d;博客主页&#xff1a;张小姐的猫~江湖背景快上车&#x1f698;&#xff0c;握好方向盘跟我有一起打天下嘞&#xff01;送给自…

Spring Boot 检索定时任务

概述 应用经常需要添加检索功能&#xff0c;开源的 ElasticSearch 是目前全文搜索引擎的首选。他可以快速的存储、搜索和分析海量数据。Spring Boot通过整合Spring Data ElasticSearch为我们提供了非常便捷的检索功能支持。 Elasticsearch是一个分布式搜索服务&#xff0c;提…

Unity3D占用内存太大怎么解决呢? -下

什么时候才是UnusedAssets?看一个例子&#xff1a; Object obj Resources.Load("MyPrefab"); GameObject instance Instantiate(obj) as GameObject; ......... Destroy(instance); 创建随后销毁了一个Prefab实例&#xff0c;这时候 MyPrefab已经没有被实际的物体…

5.XMLHttpRequest对象

XMLHttpRequest简称xhr&#xff0c;是浏览器提供的Javascript对象。之前我们使用的都是jQuery中的Ajax&#xff0c;现在我们使用原生JS的Ajax 目录 1 GET请求 1.1 不带参数请求 1.2 带参数请求 2 URL的编码与解码 2.1 编码 encodeURI() 2.2 解码 decodeURI() 3 …

【通用设计方法】之接收异常保护

目录 前言 一、接收异常保护 二、超短包、背靠背的支持 后记 前言 为了系统的鲁棒性&#xff0c;我们常常会做一系列的异常保护功能&#xff0c;避免系统挂死。 这里仅仅介绍接收保护的某些设计思路&#xff0c;抛砖引玉。 一、接收异常保护 设计思路&#xff1a;通过可配…

数据可视化大屏设计

在数据业务展示场景中&#xff0c;数据可视化大屏已经变得十分常见。那么在设计思路上&#xff0c;数据可视化大屏应当遵循什么样的设计逻辑&#xff1f;本篇文章里做了介绍&#xff0c;一起来看一下。 一、数据大屏的应用场景 1、大型会议 2、业务展示 二、数据大屏分类 1、展…

C语言源代码系列-管理系统之会员计费系统

往期文章分享点击跳转>《导航贴》- Unity手册&#xff0c;系统实战学习点击跳转>《导航贴》- Android手册&#xff0c;重温移动开发 &#x1f449;关于作者 众所周知&#xff0c;人生是一个漫长的流程&#xff0c;不断克服困难&#xff0c;不断反思前进的过程。在这个过…

【Python百日进阶-WEB开发-冲進Flask】Day183 - Flask数据库ORM基础、增加

文章目录一、day03项目环境和结构搭建1.1 flask-script1.1.1 flask-script是干什么的&#xff1f;1.1.2 flask-script安装1.1.3 flask-script的使用1.1.3.1 创建Manager实例1.1.3.2 初始化实例出错与解决1.1.4 终端启动1.1.4.1 查看runserver参数1.1.5 自定义添加manager命令1.…

linux篇【11】:linux下的线程<前序>

目录 一.linux下的线程 1.linux下的线程概念 &#xff08;1&#xff09;教材上粗略的 线程 定义 &#xff08;2&#xff09;线程的引入 &#xff08;3&#xff09;线程真正定义 以及 示意图 &#xff08;4&#xff09;linux 和 windows等其他操作系统的线程对比 &#xf…

新的趋势:From Big to Small and Wide data

新的趋势&#xff1a;From Big to Small and Wide data 所以&#xff0c;在这个时候&#xff0c;作为率先提出要做 MySQL 开源 HTAP 数据库的 StoneDB&#xff0c;想要稍微冷静一下。 不是说我们不做 HTAP 了&#xff0c;而是有了一个新的思路。这个思路&#xff0c;也同样来…

【亲测】网址引导页管理系统

介绍&#xff1a; 易航网址引导系统-网址引导页管理系统去授权版一款极其优雅的易航网址引导页管理系统&#xff0c; 如果有问题可以跟我反馈&#xff0c;共同进步。祝各位道友一路飞升&#xff0c;顶峰相见&#xff01;内置12套模板和防墙插件。 项目亮点&#xff1a; 1、…

Redis基础命令(String类型)Value为JSON

目录 String类型&#xff08;存储的值为JSON形式&#xff09; 问题&#xff1a; 解决办法&#xff1a; 示例&#xff1a; 实际操作&#xff1a; 总结&#xff1a; String类型&#xff08;存储的值为JSON形式&#xff09; 问题&#xff1a; Redis没有类似MySql中的表的概…

2022年戈登·贝尔奖授予等离子体加速器突破研究

ACM 总裁Cherri Pancake&#xff08;图片来源&#xff1a;网络&#xff09; 11月17日&#xff0c;在达拉斯举行的SC22颁奖典礼上&#xff0c;ACM将2022年戈登贝尔奖&#xff08;Gordon Bell Prize&#xff09;授予了一组研究人员&#xff0c;他们利用四台超级计算机&#xff08…

【Flink】基本转换算子使用之fliter、flatMap,键控流转换算子和分布式转换算子

文章目录一 Flink DataStream API1 基本转换算子的使用&#xff08;1&#xff09;flitera 使用匿名类实现b 使用外部类函数实现b 使用flatMap实现&#xff08;2&#xff09;flatMapa 使用匿名类实现b 使用匿名函数实现2 键控流转换算子&#xff08;1&#xff09; keyBy&#xf…

中国互联网众筹行业

近些年&#xff0c;中国互联网发展迅速&#xff0c;众筹这种起源于美国的新型互联网金融模式更是一直处于风口浪尖。在“大众创业、万众创新”的背景下&#xff0c;这种低门槛的融资模式也深受欢迎&#xff0c;加上阿里、京东、苏宁三大电商的巨头的相继入场&#xff0c;更令这…

IMS各网元的主要功能

文章目录用户注册时&#xff1a; 手机发出一个注册消息到他所在的拜访地的P。 比如&#xff0c;他是山西太原的用户&#xff0c;他这时候到了北京&#xff0c;那么这个时候&#xff0c;他要注册到IMS网络里面的话&#xff0c;这个P-CSCF就是北京的P-CSCF&#xff0c;这个北京的…

CAS号:376364-38-4,rCRAMP (rat)

rCRAMP (rat) 是一种大鼠组织蛋白酶相关的抗菌肽&#xff0c;有助于大鼠脑肽/蛋白质提取物的抗菌活性。rCRAMP (rat) 是大鼠中枢神经系统先天免疫系统的关键参与者。rCRAMP (rat) is the rat cathelin-related antimicrobial peptide. rCRAMP (rat) contributes to the antibac…

Kotlin 开发Android app(十一):Android控件RecyclerView

Android 中的控件非常的丰富&#xff0c;我们会陆陆续续的进行介绍&#xff0c;从第九节开始&#xff0c;关于Kotlin 的语法特性就差不多结束&#xff0c;后面如果有发现需要说明的语法&#xff0c;再进行相关的补充。 在Android的控件中&#xff0c;RecyclerView算是一个大控…

从 Uber 数据泄露事件我们可以学到什么?

Uber 数据泄露始于一名黑客从暗网市场购买属于一名 Uber 员工的被盗凭证。最初尝试使用这些凭据连接到 Uber 的网络失败&#xff0c;因为该帐户受 MFA 保护。为了克服这一安全障碍&#xff0c;黑客通过 What’s App 联系了 Uber 员工&#xff0c;并假装是 Uber 的安全人员&…