Java多线程(二)——ReentrantLock源码解析(补充1——从AQS中唤醒的线程)
创始人
2024-05-16 03:06:30
0

ReentrantLock源码解析(补充1)

上一章仅介绍了 ReentrantLock 的常用方法以及公平锁、非公平锁的实现。这里对上一章做一些补充。主要是:

  • AQS 中阻塞的线程被唤醒后的执行流程 (本篇讲述)

  • 可打断的锁 lock.lockInterruptibly()

  • 锁超时 lock.tryLock(long,TimeUnit)

  • 条件变量 Condition

1. AQS 中阻塞的线程被唤醒后的执行流程

线程尝试获取锁,不论公平锁还是非公平锁,如果获取不到,最后都会进入到 AQS 的队列中进行阻塞等待。直到持有锁的线程将锁释放,通过从后往前遍历查找 AQS 队列中距离 head 最近的可用节点,将其线程唤醒:LockSupport.unpark( s.thread)。 唤醒后的线程将会继续尝试竞争锁,我们分析一下它是如何竞争的:

1.1 线程被唤醒后,会继续执行 LockSupport.park() 之后的代码

LockSupport.park(thread) 方法会让Java线程进入 等待状态(WAITING),Java线程状态详情参见:

Java-线程基础

LockSupport.unpark(thread)调用之后,线程被唤醒,并获取到 CPU 时间片后,将继续运行 LockSupport.park() 位置后续的代码。(thread.interrupt()也可以将在 等待状态 线程唤醒)

在 ReentrantLock 中,AQS 中阻塞等待的线程被唤醒后,将继续下述代码的内容:

//AbstractQueuedSynchronized.java
final boolean acquireQueued(final Node node, int arg) {boolean failed = true;try {boolean interrupted = false;for (;;) {final Node p = node.predecessor();//如果当前节点的前驱为head(这是一个空节点,标志着 AQS 双向链表中的头),那么可以尝试获取锁if (p == head && tryAcquire(arg)) {setHead(node);p.next = null; // help GCfailed = false;return interrupted;}//之前被阻塞在 parkAndCheckInterrupt() 方法中//shouldParkAfterFailedAcquire()方法将node的前驱节点中废弃节点(waitStatus = CANCELLED = 1)全都清理出队列。//1. 如果有废弃节点要清理,那么该方法return false,视图再次进入循环判断当前线程所在节点是否处在队列的队头位置。//2. 如果没有废弃节点要清理,那么说明当前线程不处在队头,那么就要进入 等待状态(WAITING) 阻塞。if (shouldParkAfterFailedAcquire(p, node) &&parkAndCheckInterrupt())interrupted = true;}} finally {if (failed)cancelAcquire(node);}
}private final boolean parkAndCheckInterrupt() {//之前通过LockSupport.park()方法进入阻塞状态//LockSupport.unpark()或者interrupt()之后,会继续代码执行LockSupport.park(this);//Thread.interrupted()将会返回线程的打断标记,并且清空打断标记。return Thread.interrupted();
}

由于 LockSupport.park() 而处在 等待状态(WAITING) 的线程不仅可以通过 LockSupport.unpark() 方法唤醒,也可以通过 thread.interrupt() 方法唤醒,区别是后者将会让 thread 的打断标记置为 true。

线程唤醒后,继续执行 parkAndCheckInterrupt() 的 return 部分代码。而后进入到acquireQueued() 中继续死循环,尝试获取锁,或者重新回到 AQS 的等待队列中阻塞。

1.2 线程唤醒后的竞争分析

1. 解锁前:(node为双向指针,我画漏了)

假设thread1所在node之前所有的废弃节点(waitStatus = CANCELLED = 1)以及全部清空,当前 node(thread1) 已经处在队列头。

请添加图片描述

被唤醒时,进入 acquireQueued() 的 for(;😉 循环,由于 node(thread1) 为队头,由 acquireQueued() 代码可知,该线程将进行 tryAcquire() 尝试获取锁资源。

final boolean acquireQueued(final Node node, int arg) {...final Node p = node.predecessor();if (p == head && tryAcquire(arg)) {...}...
}

如果没有出现竞争,如上一章讨论的上锁流程, node(thread1) 将会成为新的空head,thread1 从 node 中脱离出来,继续执行后续临界区代码。

本篇中,我们补充讨论出现竞争的情况:

2. 解锁后,出现竞争:(node为双向指针,我画漏了)

请添加图片描述

node(thread1) 被唤醒后,由于处在空head之后,为首个可用队头节点,将进入到 tryAcquire() 尝试获取所资源,同时 thread3 也在尝试获取锁资源。

3. 竞争失败 (node为双向指针,我画漏了)

请添加图片描述

如果 node(thread1) 竞争失败,将会进入 acquireQueue() 中下列部分,再次通过 parkAndCheckInterrupt() 的 LockSupport.lock() 方法阻塞起来。需要注意的是,这里没有额外动作,node(thread1) 在队列中的位置不变:

final boolean acquireQueued(final Node node, int arg) {try {for (;;) {final Node p = node.predecessor();if (p == head && tryAcquire(arg)) {...}// tryAquire()竞争失败 return false后,进入下面部分if (shouldParkAfterFailedAcquire(p, node) &&//再次阻塞起来parkAndCheckInterrupt())interrupted = true;}} finally {if (failed)cancelAcquire(node);}
}

4.竞争成功

请添加图片描述

竞争成功,thread1将会从node中解放出来,进入临界区运行后续代码:

Thread thread1 = new Thread(()->{lock.lock();//获取到锁之后,进入下面的临界区代码try{//临界区代码}finally{lock.unlock();}
});

而 node 将会被置空,并成为新的 空head 节点,原先的 空head 节点被抛弃:

final boolean acquireQueued(final Node node, int arg) {...if (p == head && tryAcquire(arg)) {//争锁成功,成为新的 空headsetHead(node);//将原先的空head抛弃p.next = null; // help GCfailed = false;return interrupted;}...     
}private void setHead(Node node) {//成为新的headhead = node;//将封装在 node 中的 thread 解放出去node.thread = null;//清空prev指向node.prev = null;
}

而竞争失败的 thread3 将会如上一篇所言,进入 AQS 等待队列,并通过 LockSupport.park() 进入 等待状态(WAITING)。

相关内容

热门资讯

监控摄像头接入GB28181平... 流程简介将监控摄像头的视频在网站和APP中直播,要解决的几个问题是:1&...
Windows10添加群晖磁盘... 在使用群晖NAS时,我们需要通过本地映射的方式把NAS映射成本地的一块磁盘使用。 通过...
protocol buffer... 目录 目录 什么是protocol buffer 1.protobuf 1.1安装  1.2使用...
在Word、WPS中插入AxM... 引言 我最近需要写一些文章,在排版时发现AxMath插入的公式竟然会导致行间距异常&#...
【PdgCntEditor】解... 一、问题背景 大部分的图书对应的PDF,目录中的页码并非PDF中直接索引的页码...
Fluent中创建监测点 1 概述某些仿真问题,需要创建监测点,用于获取空间定点的数据࿰...
educoder数据结构与算法...                                                   ...
MySQL下载和安装(Wind... 前言:刚换了一台电脑,里面所有东西都需要重新配置,习惯了所...
修复 爱普生 EPSON L4... L4151 L4153 L4156 L4158 L4163 L4165 L4166 L4168 L4...
MFC文件操作  MFC提供了一个文件操作的基类CFile,这个类提供了一个没有缓存的二进制格式的磁盘...