转自:http://blog.csdn.net/chen77716/article/details/6641477
前文(深入JVM锁机制-synchronized)分析了JVM中的synchronized实现,本文继续分析JVM中的另一种锁Lock的实现。与synchronized不同的是,Lock完全用Java写成,在java这个层面是无关JVM实现的。
在java.util.concurrent.locks包中有很多Lock的实现类,常用的有ReentrantLock、ReadWriteLock(实现类ReentrantReadWriteLock),其实现都依赖java.util.concurrent.AbstractQueuedSynchronizer类,实现思路都大同小异,因此我们以ReentrantLock作为讲解切入点。
1. ReentrantLock的调用过程
经过观察ReentrantLock把所有Lock接口的操作都委派到一个Sync类上,该类继承了AbstractQueuedSynchronizer:
- static abstract class Sync extends AbstractQueuedSynchronizer
Sync又有两个子类:
- final static class NonfairSync extends Sync
- final static class FairSync extends Sync
显然是为了支持公平锁和非公平锁而定义,默认情况下为非公平锁。
先理一下Reentrant.lock()方法的调用过程(默认非公平锁):
这些讨厌的Template模式导致很难直观的看到整个调用过程,其实通过上面调用过程及AbstractQueuedSynchronizer的注释可以发现,AbstractQueuedSynchronizer中抽象了绝大多数Lock的功能,而只把tryAcquire方法延迟到子类中实现。tryAcquire方法的语义在于用具体子类判断请求线程是否可以获得锁,无论成功与否AbstractQueuedSynchronizer都将处理后面的流程。
2. 锁实现(加锁)
简单说来,AbstractQueuedSynchronizer会把所有的请求线程构成一个CLH队列,当一个线程执行完毕(lock.unlock())时会激活自己的后继节点,但正在执行的线程并不在队列中,而那些等待执行的线程全部处于阻塞状态,经过调查线程的显式阻塞是通过调用LockSupport.park()完成,而LockSupport.park()则调用sun.misc.Unsafe.park()本地方法,再进一步,HotSpot在Linux中中通过调用pthread_mutex_lock函数把线程交给系统内核进行阻塞。
该队列如图:
与synchronized相同的是,这也是一个虚拟队列,不存在队列实例,仅存在节点之间的前后关系。令人疑惑的是为什么采用CLH队列呢?原生的CLH队列是用于自旋锁,但Doug Lea把其改造为阻塞锁。
当有线程竞争锁时,该线程会首先尝试获得锁,这对于那些已经在队列中排队的线程来说显得不公平,这也是非公平锁的由来,与synchronized实现类似,这样会极大提高吞吐量。
如果已经存在Running线程,则新的竞争线程会被追加到队尾,具体是采用基于CAS的Lock-Free算法,因为线程并发对Tail调用CAS可能会导致其他线程CAS失败,解决办法是循环CAS直至成功。AbstractQueuedSynchronizer的实现非常精巧,令人叹为观止,不入细节难以完全领会其精髓,下面详细说明实现过程:
2.1 Sync.nonfairTryAcquire
nonfairTryAcquire方法将是lock方法间接调用的第一个方法,每次请求锁时都会首先调用该方法。
- final boolean nonfairTryAcquire(int acquires) {
- final Thread current = Thread.currentThread();
- int c = getState();
- if (c == 0) {
- if (compareAndSetState(0, acquires)) {
- setExclusiveOwnerThread(current);
- return true;
- }
- }
- else if (current == getExclusiveOwnerThread()) {
- int nextc = c + acquires;
- if (nextc < 0) // overflow
- throw new Error("Maximum lock count exceeded");
- setState(nextc);
- return true;
- }
- return false;
- }
该方法会首先判断当前状态,如果c==0说明没有线程正在竞争该锁,如果不c !=0 说明有线程正拥有了该锁。
如果发现c==0,则通过CAS设置该状态值为acquires,acquires的初始调用值为1,每次线程重入该锁都会+1,每次unlock都会-1,但为0时释放锁。如果CAS设置成功,则可以预计其他任何线程调用CAS都不会再成功,也就认为当前线程得到了该锁,也作为Running线程,很显然这个Running线程并未进入等待队列。
如果c !=0 但发现自己已经拥有锁,只是简单地++acquires,并修改status值,但因为没有竞争,所以通过setStatus修改,而非CAS,也就是说这段代码实现了偏向锁的功能,并且实现的非常漂亮。
2.2 AbstractQueuedSynchronizer.addWaiter
addWaiter方法负责把当前无法获得锁的线程包装为一个Node添加到队尾:
- private Node addWaiter(Node mode) {
- Node node = new Node(Thread.currentThread(), mode);
- // Try the fast path of enq; backup to full enq on failure
- Node pred = tail;
- if (pred != null) {
- node.prev = pred;
- if (compareAndSetTail(pred, node)) {
- pred.next = node;
- return node;
- }
- }
- enq(node);
- return node;
- }
其中参数mode是独占锁还是共享锁,默认为null,独占锁。追加到队尾的动作分两步:
- 如果当前队尾已经存在(tail!=null),则使用CAS把当前线程更新为Tail
- 如果当前Tail为null或则线程调用CAS设置队尾失败,则通过enq方法继续设置Tail
下面是enq方法:
- private Node enq(final Node node) {
- for (;;) {
- Node t = tail;
- if (t == null) { // Must initialize
- Node h = new Node(); // Dummy header
- h.next = node;
- node.prev = h;
- if (compareAndSetHead(h)) {
- tail = node;
- return h;
- }
- }
- else {
- node.prev = t;
- if (compareAndSetTail(t, node)) {
- t.next = node;
- return t;
- }
- }
- }
- }
该方法就是循环调用CAS,即使有高并发的场景,无限循环将会最终成功把当前线程追加到队尾(或设置队头)。总而言之,addWaiter的目的就是通过CAS把当前现在追加到队尾,并返回包装后的Node实例。
把线程要包装为Node对象的主要原因,除了用Node构造供虚拟队列外,还用Node包装了各种线程状态,这些状态被精心设计为一些数字值:
- SIGNAL(-1) :线程的后继线程正/已被阻塞,当该线程release或cancel时要重新这个后继线程(unpark)
- CANCELLED(1):因为超时或中断,该线程已经被取消
- CONDITION(-2):表明该线程被处于条件队列,就是因为调用了Condition.await而被阻塞
- PROPAGATE(-3):传播共享锁
- 0:0代表无状态
2.3 AbstractQueuedSynchronizer.acquireQueued
acquireQueued的主要作用是把已经追加到队列的线程节点(addWaiter方法返回值)进行阻塞,但阻塞前又通过tryAccquire重试是否能获得锁,如果重试成功能则无需阻塞,直接返回
- final boolean acquireQueued(final Node node, int arg) {
- try {
- boolean interrupted = false;
- for (;;) {
- final Node p = node.predecessor();
- if (p == head && tryAcquire(arg)) {
- setHead(node);
- p.next = null; // help GC
- return interrupted;
- }
- if (shouldParkAfterFailedAcquire(p, node) &&
- parkAndCheckInterrupt())
- interrupted = true;
- }
- } catch (RuntimeException ex) {
- cancelAcquire(node);
- throw ex;
- }
- }
仔细看看这个方法是个无限循环,感觉如果p == head && tryAcquire(arg)条件不满足循环将永远无法结束,当然不会出现死循环,奥秘在于第12行的parkAndCheckInterrupt会把当前线程挂起,从而阻塞住线程的调用栈。
- private final boolean parkAndCheckInterrupt() {
- LockSupport.park(this);
- return Thread.interrupted();
- }
如前面所述,LockSupport.park最终把线程交给系统(Linux)内核进行阻塞。当然也不是马上把请求不到锁的线程进行阻塞,还要检查该线程的状态,比如如果该线程处于Cancel状态则没有必要,具体的检查在shouldParkAfterFailedAcquire中:
- 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.
- */
- do {
- node.prev = pred = pred.prev;
- } while (pred.waitStatus > 0);
- pred.next = node;
- } else {
- /*
- * 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.
- */
- compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
- }
- return false;
- }
检查原则在于:
- 规则1:如果前继的节点状态为SIGNAL,表明当前节点需要unpark,则返回成功,此时acquireQueued方法的第12行(parkAndCheckInterrupt)将导致线程阻塞
- 规则2:如果前继节点状态为CANCELLED(ws>0),说明前置节点已经被放弃,则回溯到一个非取消的前继节点,返回false,acquireQueued方法的无限循环将递归调用该方法,直至规则1返回true,导致线程阻塞
- 规则3:如果前继节点状态为非SIGNAL、非CANCELLED,则设置前继的状态为SIGNAL,返回false后进入acquireQueued的无限循环,与规则2同
总体看来,shouldParkAfterFailedAcquire就是靠前继节点判断当前线程是否应该被阻塞,如果前继节点处于CANCELLED状态,则顺便删除这些节点重新构造队列。
至此,锁住线程的逻辑已经完成,下面讨论解锁的过程。
3. 解锁
请求锁不成功的线程会被挂起在acquireQueued方法的第12行,12行以后的代码必须等线程被解锁锁才能执行,假如被阻塞的线程得到解锁,则执行第13行,即设置interrupted = true,之后又进入无限循环。
从无限循环的代码可以看出,并不是得到解锁的线程一定能获得锁,必须在第6行中调用tryAccquire重新竞争,因为锁是非公平的,有可能被新加入的线程获得,从而导致刚被唤醒的线程再次被阻塞,这个细节充分体现了“非公平”的精髓。通过之后将要介绍的解锁机制会看到,第一个被解锁的线程就是Head,因此p == head的判断基本都会成功。
至此可以看到,把tryAcquire方法延迟到子类中实现的做法非常精妙并具有极强的可扩展性,令人叹为观止!当然精妙的不是这个Templae设计模式,而是Doug Lea对锁结构的精心布局。
解锁代码相对简单,主要体现在AbstractQueuedSynchronizer.release和Sync.tryRelease方法中:
class AbstractQueuedSynchronizer
- public final boolean release(int arg) {
- if (tryRelease(arg)) {
- Node h = head;
- if (h != null && h.waitStatus != 0)
- unparkSuccessor(h);
- return true;
- }
- return false;
- }
class Sync
- 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;
- }
tryRelease与tryAcquire语义相同,把如何释放的逻辑延迟到子类中。tryRelease语义很明确:如果线程多次锁定,则进行多次释放,直至status==0则真正释放锁,所谓释放锁即设置status为0,因为无竞争所以没有使用CAS。
release的语义在于:如果可以释放锁,则唤醒队列第一个线程(Head),具体唤醒代码如下:
- 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);
- }
这段代码的意思在于找出第一个可以unpark的线程,一般说来head.next == head,Head就是第一个线程,但Head.next可能被取消或被置为null,因此比较稳妥的办法是从后往前找第一个可用线程。貌似回溯会导致性能降低,其实这个发生的几率很小,所以不会有性能影响。之后便是通知系统内核继续该线程,在Linux下是通过pthread_mutex_unlock完成。之后,被解锁的线程进入上面所说的重新竞争状态。
4. Lock VS Synchronized
AbstractQueuedSynchronizer通过构造一个基于阻塞的CLH队列容纳所有的阻塞线程,而对该队列的操作均通过Lock-Free(CAS)操作,但对已经获得锁的线程而言,ReentrantLock实现了偏向锁的功能。
synchronized的底层也是一个基于CAS操作的等待队列,但JVM实现的更精细,把等待队列分为ContentionList和EntryList,目的是为了降低线程的出列速度;当然也实现了偏向锁,从数据结构来说二者设计没有本质区别。但synchronized还实现了自旋锁,并针对不同的系统和硬件体系进行了优化,而Lock则完全依靠系统阻塞挂起等待线程。
当然Lock比synchronized更适合在应用层扩展,可以继承AbstractQueuedSynchronizer定义各种实现,比如实现读写锁(ReadWriteLock),公平或不公平锁;同时,Lock对应的Condition也比wait/notify要方便的多、灵活的多。
相关推荐
内容概要:本文详细介绍了利用西门子S7-200 PLC和组态王软件构建的自动配料装车系统。主要内容涵盖梯形图程序的设计,包括配料逻辑、启动条件、PID控制优化以及信号处理方法;接线图和IO分配的具体细节,如输入输出端口的功能定义、模拟量模块的应用;组态王的画面设计及其与PLC的数据交互方式。此外,还分享了一些实用的调试经验和维护技巧,如解决通信干扰问题的方法。 适用人群:自动化工程技术人员、PLC编程爱好者、工业控制系统集成商。 使用场景及目标:适用于需要提高生产效率的企业,尤其是那些希望通过引入先进的自动化技术来减少人为错误并提升作业精度的场合。目标是在确保安全的前提下,实现高效稳定的物料配送流程。 其他说明:文中提供的实例不仅有助于理解理论概念,还能指导实际项目的实施。对于希望深入掌握PLC编程和HMI界面开发的人来说,是一份非常有价值的学习资料。
内容概要:本文详细介绍了基于二/三自由度动力学模型的MPC(模型预测控制)在自动驾驶中的应用,重点讨论了任意路径跟踪技术和Carsim-Simulink联合仿真的实现方法。首先解释了MPC的基本概念及其在自动驾驶中的重要性,然后阐述了二/三自由度动力学模型的作用。接着展示了如何利用Carsim和Simulink进行联合仿真,以实现对自定义路径的精确跟踪。最后提供了简化的代码片段,演示了MPC控制器的工作流程,并展望了未来的发展方向。 适合人群:从事自动驾驶技术研发的专业人士,尤其是对MPC和车辆动力学感兴趣的工程师和技术研究人员。 使用场景及目标:适用于需要深入了解自动驾驶路径跟踪技术的研究机构和企业,旨在提升车辆在复杂路况下的自主导航能力和安全性。 其他说明:文中提到的技术和工具对于推动自动驾驶技术的进步具有重要意义,同时也为相关领域的创新提供了理论支持和技术指导。
实训商业源码-苹果CMS V10仿乐琪影视网站模板-毕业设计.zip
为开发人员打造的低代码开发平台。Mybatis-plus关联查询,关联无SQL,性能高10倍,前后端代码本地可视化生成,flowable工作流,spring cloud微服务,强大的无代码模型表单页面流程设计能力等全方位赋能!
更新后的库,不会死机,不含代码以及其他各种信息
实训商业源码-语音变声器-毕业设计.zip
实训商业源码-坤坤鸡乐盒小程序-毕业设计.zip
数学复习
实训商业源码-微信自动发卡商业小程序-毕业设计.zip
数据集介绍:高空视角飞机跑道船只目标检测数据集 一、基础信息 数据集名称:高空视角飞机跑道船只目标检测数据集 图片数量: - 训练集:3,375张图片 - 验证集:331张图片 - 测试集:164张图片 分类类别: - airplane:涵盖多种机型的高空目标检测样本 - runway:包含机场跑道及地面辅助设施的关键区域标注 - ship:覆盖不同尺寸和航向的船只检测样本 标注格式: YOLO格式,包含目标检测所需的归一化坐标及类别标签 数据特性: - 无人机及高空平台采集视角 - 覆盖陆地、海洋、机场等多场景 - 包含目标小尺寸、密集排列等真实检测挑战 二、适用场景 航空交通管理系统开发: 支持构建自动识别空中飞行器与地面跑道的AI模型,提升空域管理效率 无人机自主导航系统: 为无人机提供机场跑道识别与障碍物避让的基准训练数据 港口船舶监控解决方案: 训练船舶检测模型,支持海上交通流量统计与异常行为识别 遥感图像分析工具: 适用于卫星/航拍影像中的基础设施识别与地理信息系统开发 三、数据集优势 多目标协同检测能力: 同时包含空中目标(飞机)、地面设施(跑道)、海上目标(船舶)的关联场景数据 高适应性标注: 兼容YOLOv5/YOLOv8等主流目标检测框架,支持快速模型迭代 视角多样性: 涵盖不同高度、角度、光照条件下的无人机及高空拍摄视角 专业数据分割: 严格划分训练集/验证集/测试集,符合工业级模型开发标准
内容概要:本文详细介绍了锂电行业中C#通信库的开发及其应用。首先概述了锂电行业背景下主流PLC(如三菱、欧姆龙)的通信需求,接着具体分析了C#通信库文件中涉及的各种通信程序,包括PLC通信程序、电池测试仪器通信程序、扫码枪通信程序以及到期锁机控制程序。文中还探讨了模块化设计、数据处理优化、安全保障和兼容性优化等方面的建议,旨在提升软件性能和稳定性。 适合人群:从事锂电行业自动化控制系统的开发人员、工程师和技术爱好者。 使用场景及目标:①理解和掌握锂电行业中C#通信库的具体实现方法;②应用于实际生产环境中,实现高效的数据采集、控制及通信任务;③优化现有系统,提高软件性能和安全性。 其他说明:本文不仅提供了详细的通信程序源代码,还提出了多项优化建议,帮助开发者更好地应对工业自动化控制领域的挑战。
实训商业源码-王者荣耀故事站小程序含vue后台-毕业设计.zip
实训商业源码--原生开发淘系统-毕业设计.zip
内容概要:本文详细介绍了如何利用Matlab对一维实验数据进行相空间重构,重点在于确定延迟时间t和嵌入维数m这两个关键参数。文中采用互信息法来确定延迟时间,并通过计算不同嵌入维数下重构相空间的拓扑性质选择合适的嵌入维数。此外,还探讨了如何通过计算关联维数来验证一维实验数据是否具有混沌特性。关联维数的计算涉及将一维时间序列转化为高维数据,再计算各点间距离矩阵及其关联积分,最终得出关联维数并判断是否存在混沌现象。 适合人群:从事非线性动力学、复杂系统研究的专业人士,尤其是需要处理一维实验数据的研究人员。 使用场景及目标:适用于希望深入了解相空间重构理论及其应用的研究者,旨在帮助他们掌握用Matlab实现相空间重构的具体方法,从而更好地理解和分析复杂系统的混沌特征。 其他说明:本文不仅提供了一种有效的技术手段,而且强调了理论与实际操作相结合的重要性,鼓励读者在实践中不断探索和完善自己的研究思路。
内容概要:本文详细介绍了如何利用COMSOL软件进行固体超声导波的二维仿真。主要内容包括仿真的准备工作,如设定材料属性和模型尺寸;汉宁窗调制的5周期正弦函数作为激励信号的具体实现方法及其优势;以及如何通过指定位移来添加激励信号。此外,还讨论了仿真的过程和结果分析,展示了声波在固体中的传播路径和振幅分布等情况。 适合人群:对超声波仿真感兴趣的科研人员、工程技术人员以及相关专业的学生。 使用场景及目标:适用于需要研究声波在固体中传播特性的项目,旨在提高对超声导波的理解并优化仿真效果。 其他说明:文中提供了具体的代码片段用于生成汉宁窗调制的正弦波形,有助于读者快速上手实践。同时强调了汉宁窗调制对于减少信号边缘效应的重要作用。
太阳影子定 位模型的研究及应用.pdf
实训商业源码-聚合登录平台网站源码-毕业设计.zip
内容概要:本文详细介绍了如何利用多尺度一维卷积神经网络(MS-1DCNN)在PyTorch框架下进行轴承故障诊断。首先,通过对凯斯西储大学(CWRU)提供的轴承数据集进行预处理,提取振动信号并将其转换为适合模型输入的格式。然后,构建了一个包含三个不同尺度卷积核的MS-1DCNN模型,能够捕捉到不同时间尺度的特征。接下来,采用AdamW优化器和余弦退火学习率调度器对模型进行了训练,并加入了早停机制以避免过拟合。最终,在验证集上实现了超过97.5%的高精度。此外,还展示了如何使用混淆矩阵对预测结果进行可视化。 适合人群:对机器学习尤其是深度学习感兴趣的初学者以及从事机械故障诊断的研究人员。 使用场景及目标:本教程旨在帮助读者掌握从数据准备到模型部署的完整故障诊断流程,特别适合希望快速入门故障诊断领域的学生和技术人员。 其他说明:文中提供了详细的代码片段,涵盖了数据读取、预处理、模型定义、训练及评估等多个方面,确保读者能够复现实验结果。同时,针对可能出现的问题给出了相应的解决方案。
内容概要:本文介绍了PSIM的DC-DC仿真技术及其在降压斩波电路Buck中的应用。首先阐述了电力电子技术的发展背景,接着详细解释了PSIM作为一种集成化的仿真工具的功能特点,特别是在DC-DC变换器仿真方面的作用。然后重点讲解了降压斩波电路Buck的基本概念、特点及其广泛应用领域,包括电源适配器、UPS电源、车载电源等。最后探讨了利用PSIM进行降压斩波电路仿真的具体方法和技术要点,强调了仿真对于理解和优化电路设计的重要性。 适合人群:对电力电子感兴趣的初学者,尤其是希望深入了解DC-DC变换器和降压斩波电路的技术爱好者。 使用场景及目标:适用于希望通过理论学习和实际操作相结合的方式掌握电力电子基础知识的人群。目标是在实践中理解降压斩波电路的工作原理,学会使用PSIM进行电路仿真,从而提升电路设计能力。 其他说明:文中不仅提供了详细的理论介绍,还结合实例展示了具体的仿真步骤,帮助读者更好地理解和应用所学知识。
内容概要:本文详细介绍了利用Matlab及其图形用户界面(GUI)对语音信号进行去噪处理的方法。主要内容包括加载语音信号并显示其时域和频域图,随后依次添加正弦噪声和高斯白噪声,并采用巴特沃斯低通滤波器和小波变换两种方法进行去噪处理。每一步处理后均可播放处理后的音频,以便直观感受去噪效果。此外,文中还强调了GUI界面在提高操作便捷性和结果可视化方面的重要作用。 适合人群:对语音信号处理感兴趣的初学者以及有一定Matlab基础的研发人员。 使用场景及目标:适用于需要对语音信号进行预处理的研究项目或实际应用场景,如语音识别系统、通信工程等领域。目标是掌握Matlab中常用的去噪技术和工具,提升语音信号质量。 其他说明:建议使用Matlab 2022b及以上版本,低版本可能无法兼容部分功能。同时提供了非GUI版本的代码供选择。