JVM 源码分析(四):深入理解 park / unpark
前言
熟悉 Java 并发包的人一定对 LockSupport 的 park/unpark
方法不会感到陌生,它是 Lock(AQS)的基石,给 Lock(AQS)提供了挂起/恢复当前线程的能力。
LockSupport 的 park/unpark
方法本质上是对 Unsafe 的 park/unpark
方法的简单封装,而后者是 native 方法,对 Java 程序来说是一个黑箱操作,那么要想了解它的底层实现,就必须深入 Java 虚拟机的源码。
本篇将介绍 park/unpark
方法在 Hotsport 虚拟机中的具体实现。
Parker 源码调试与分析
在 Hotspot 源码中,unsafe.cpp 文件专门用于为 Java Unsafe 类中的各种 native 方法提供具体实现。
其中 park
方法的实现代码如下:
unpark
方法的实现代码如下:
两者的核心操作都是通过委托当前线程所关联的 Parker 对象来完成的(每个线程都会关联一个自己的 Parker 对象),于是,Parker 对象的 park/unpark
方法就成为了我们的焦点。
下面我将联合 Java 程序与 Hotspot 源码一起调试,观察 Parker 对象的 park/unpark
方法的内部操作。
其中 Java 程序的代码如下:
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
System.out.println("park开始");
LockSupport.park();
System.out.println("park结束");
}, "t1");
Thread t2 = new Thread(() -> {
System.out.println("unpark开始");
LockSupport.unpark(t1);
System.out.println("unpark结束");
}, "t2");
Scanner scanner = new Scanner(System.in);
String input;
System.out.println("输入“1”启动t1线程,输入“2”启动t2线程,输入“quit”退出");
while (!(input = scanner.nextLine()).equals("quit")) {
if (input.equals("1")) {
if (t1.getState().equals(Thread.State.NEW)) {
t1.start();
}
} else if (input.equals("2")) {
if (t2.getState().equals(Thread.State.NEW)) {
t2.start();
}
}
}
}
我们采用远程调试的方式运行上面的 Java 程序,然后通过在控制台输入“1” 来启动 t1 线程。当 t1 线程启动后,LockSupport.park
方法就会得以执行。
如图所示,当前 t1 线程停在了断点处,即停在了 Parker::park
方法的第一条语句上。
我们来分析一下该方法主要做的事情。
它首先利用一个原子交换操作将计数器的值改为 0,同时检查计数器的原值是否大于 0,如果大于 0,表示当前 Parker 对象的 unpark 方法先于 park 方法执行了(因为 unpark 方法会把计数器的值改为 1),那么本次 park 方法将直接返回,表示取消本次操作。如果计数器的原值不大于 0,则继续往下执行。
接着判断当前线程是否被标记了中断,如果是的话就直接返回,否则就通过 pthread_mutex_trylock
函数尝试加 mutex 锁,如果加锁失败也直接返回。(pthread_mutex_trylock
函数是一个系统调用,它会针对操作系统的一个互斥量进行加锁,加锁成功将返回 0)。
在我们的调试中,以上所有条件判断都不命中,于是线程顺利地执行到了下图所示的位置。
图中断点处的代码相当关键,它完成了对 pthread_cond_wait
函数的调用,该函数是 Linux 标准线程库(libpthread.so)中的一个系统调用,它会使当前线程加入操作系统的条件等待队列,同时释放 mutex 锁并使当前线程挂起。
Java 中的
wait
和await
方法提供了和pthread_cond_wait
函数同样的功能,前者本质上是对后者的封装。如果对pthread_cond_wait
函数的具体实现感兴趣,可以参考: https://code.woboq.org/userspace/glibc/nptl/pthread_cond_wait.c.html
由于 pthread_cond_wait
函数会使当前线程挂起,所以在我点击 "Step Over" 之后,线程阻塞在了 pthread_cond_wait
函数上,并等待被唤醒。
下图显示了通过 jstack 命令打印的线程堆栈信息,可以看到 t1 线程已经处于 waiting (parking) 状态。
至此,park 操作暂时告一段落。
接下来,我们通过在控制台输入“2” 来启动 t2 线程。当 t2 线程启动后,LockSupport.unpark(t1)
就会得以执行。
如图所示,当前 t2 线程停在了断点处,即停在了 Parker::unpark
方法的第二行代码上。
该方法做的事情相对简单,它先是给当前线程加锁,然后将计数器的值改为 1,接着判断 Parker 对象所关联的线程是否被 park,如果是,则通过 pthread_mutex_signal
函数唤醒该线程,最后释放锁。
pthread_mutex_signal
函数通常与 pthread_cond_wait
函数配套使用,其作用是唤醒操作系统中在某个条件变量上等待着的线程。
当 unpark 操作完成后,之前被 park 的线程将恢复至运行状态(需要先拿到 mutex 锁),然后从 pthread_cond_wait
方法中返回,接着执行剩余代码。下图显示了Parker::park
方法的剩余代码。
可以看到,当线程恢复运行后,计数器的值会再次被置为 0,然后线程会释放锁,并结束整个 park 操作。
park/unpark 原理总结
每个线程都会关联一个 Parker 对象,每个 Parker 对象都各自维护了三个角色:计数器、互斥量、条件变量。
park 操作:
获取当前线程关联的 Parker 对象。 将计数器置为 0,同时检查计数器的原值是否为 1,如果是则放弃后续操作。 在互斥量上加锁。 在条件变量上阻塞,同时释放锁并等待被其他线程唤醒,当被唤醒后,将重新获取锁。 当线程恢复至运行状态后,将计数器的值再次置为 0。 释放锁。
unpark 操作:
获取目标线程关联的 Parker 对象(注意目标线程不是当前线程)。 在互斥量上加锁。 将计数器置为 1。 唤醒在条件变量上等待着的线程。 释放锁。
补充:jstack 命令和 kill 命令
jstack 命令会给 Java 虚拟机进程发送一个 SIGQUIT 信号,当 Java 虚拟机收到信号后,会另起一个线程专门执行打印线程堆栈的任务。如图,从 GDB
标签页中可以观察到 SIGQUIT 信号。
在 Linux 中使用 kill -3 命令也可以实现和 jstack 命令几乎一样的效果,这是因为 kill 命令本身就是一个用于给进程发送信号的工具,只不过默认发送的是 SIGTERM 信号(终止信号),该信号用于终止一个进程。可以通过 kill -l 命令查看所有可用信号,kill -3 表示发送 SIGQUIT 信号。
JVM 源码分析(四):深入理解 park / unpark的更多相关文章
- JVM源码分析之Metaspace解密
概述 metaspace,顾名思义,元数据空间,专门用来存元数据的,它是jdk8里特有的数据结构用来替代perm,这块空间很有自己的特点,前段时间公司这块的问题太多了,主要是因为升级了中间件所 ...
- JVM源码分析之synchronized实现
“365篇原创计划”第十二篇. 今天呢!灯塔君跟大家讲: JVM源码分析之synchronized实现 java内部锁synchronized的出现,为多线程的并发执行提供了一个稳定的 ...
- JVM源码分析之一个Java进程究竟能创建多少线程
JVM源码分析之一个Java进程究竟能创建多少线程 原创: 寒泉子 你假笨 2016-12-06 概述 虽然这篇文章的标题打着JVM源码分析的旗号,不过本文不仅仅从JVM源码角度来分析,更多的来自于L ...
- JVM源码分析之堆外内存完全解读
JVM源码分析之堆外内存完全解读 寒泉子 2016-01-15 17:26:16 浏览6837 评论0 阿里技术协会 摘要: 概述 广义的堆外内存 说到堆外内存,那大家肯定想到堆内内存,这也是我们 ...
- JVM源码分析-JVM源码编译与调试
要分析JVM的源码,结合资料直接阅读是一种方式,但是遇到一些想不通的场景,必须要结合调试,查看执行路径以及参数具体的值,才能搞得明白.所以我们先来把JVM的源码进行编译,并能够使用GDB进行调试. 编 ...
- JVM源码分析之警惕存在内存泄漏风险的FinalReference(增强版)
概述 JAVA对象引用体系除了强引用之外,出于对性能.可扩展性等方面考虑还特地实现了四种其他引用:SoftReference.WeakReference.PhantomReference.FinalR ...
- JVM源码分析之JVM启动流程
原创申明:本文由公众号[猿灯塔]原创,转载请说明出处标注 “365篇原创计划”第十四篇. 今天呢!灯塔君跟大家讲: JVM源码分析之JVM启动流程 前言: 执行Java类的main方法,程序就能运 ...
- JVM源码分析之Object.wait/notify实现
“365篇原创计划”第十一篇. 今天呢!灯塔君跟大家讲: JVM源码分析之Object.wait/notify实现 最简单的东西,往往包含了最复杂的实现,因为需要为上层的存在提 ...
- 学习JUC源码(3)——Condition等待队列(源码分析结合图文理解)
前言 在Java多线程中的wait/notify通信模式结尾就已经介绍过,Java线程之间有两种种等待/通知模式,在那篇博文中是利用Object监视器的方法(wait(),notify().notif ...
- JVM源码分析之SystemGC完全解读
JVM源码分析之SystemGC完全解读 概述 JVM的GC一般情况下是JVM本身根据一定的条件触发的,不过我们还是可以做一些人为的触发,比如通过jvmti做强制GC,通过System.gc触发,还可 ...
随机推荐
- 【AtCoder AGC023F】01 on Tree(贪心)
Description 给定一颗 \(n\) 个结点的树,每个点有一个点权 \(v\).点权只可能为 \(0\) 或 \(1\). 现有一个空数列,每次可以向数列尾部添加一个点 \(i\) 的点权 \ ...
- Ubuntu18 安装 MySQL 8.0.22
Ubuntu18 安装 MySQL 8.0.22 网上教程都比旧,也不是第一次安装了,但依然还是花了比较多的时间,特此记录本次安装过程.因是安装完毕后回忆记录,或有错漏. 第一步: 下载 mysql ...
- Java-静态关键字Static&静态代码块
静态成员变量:(static修饰变量) 如果一个成员变量使用了static关键字,那么这个变量不再属于对象自己,而是属于所在的类.多个对象共享同一份数据. 静态方法:(static修饰方法) 一旦使用 ...
- 前端js部署
1 执行命令 cnpm run build 2.2 提取dist静态资源 将静态资源放置后端static下 /static文件是django后端的部署文件夹 3 Nginx写入配置文件 写入etc ...
- JavaSE17-File&递归&字节流
1.File类 1.1 File类概述和构造方法 File类介绍 它是文件和目录路径名的抽象表示 文件和目录是可以通过File封装成对象的 对于File而言,其封装的并不是一个真正存在的文件,仅仅是一 ...
- IO流(03)--序列化流、打印流
序列化流 Java提供了一种对象序列化的机制,用一个字节序列可以表示一个对象,该字节序列包含该对象的数据.对象的类型和对象中存储的属性等信息.字节序列写入到文件中后,就相当于在文件中保存了一个对象信息 ...
- Sharding-JDBC使用jasypt3.0及以上版本加密数据库连接密码
本文中介绍的是基于Sharding-JDBC 4.0和jasypt 3.0及其以上版本对数据库连接密码进行加密操作 引入依赖 项目的pom.xml中引入maven依赖 <dependency&g ...
- Git 使用中遇见的各种问题及解决办法
一.修改提交代码的用户名以及提交邮箱,(推荐使用方法2,一劳永逸) 方法1(修改.git/config文件): step1:进入工程.git文件夹 step2:vim config step3:末行添 ...
- 微服务 - 服务注册发现(三)Consule
Consul 集群 在consul方案中,每个提供服务的节点上都要部署和运行consul的agent,所有运行consul agent节点的集合构成consul cluster.consul agen ...
- svn忽略idea生成的本地配置文件
为根目录添加svn属性svn:global-ignores 值为 *.iml .idea 多个值之间用换行分隔