前言

熟悉 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 中的 waitawait 方法提供了和 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 操作:

  1. 获取当前线程关联的 Parker 对象。
  2. 将计数器置为 0,同时检查计数器的原值是否为 1,如果是则放弃后续操作。
  3. 在互斥量上加锁。
  4. 在条件变量上阻塞,同时释放锁并等待被其他线程唤醒,当被唤醒后,将重新获取锁。
  5. 当线程恢复至运行状态后,将计数器的值再次置为 0。
  6. 释放锁。

unpark 操作:

  1. 获取目标线程关联的 Parker 对象(注意目标线程不是当前线程)。
  2. 在互斥量上加锁。
  3. 将计数器置为 1。
  4. 唤醒在条件变量上等待着的线程。
  5. 释放锁。

补充:jstack 命令和 kill 命令

jstack 命令会给 Java 虚拟机进程发送一个 SIGQUIT 信号,当 Java 虚拟机收到信号后,会另起一个线程专门执行打印线程堆栈的任务。如图,从 GDB 标签页中可以观察到 SIGQUIT 信号。

在 Linux 中使用 kill -3 命令也可以实现和 jstack 命令几乎一样的效果,这是因为 kill 命令本身就是一个用于给进程发送信号的工具,只不过默认发送的是 SIGTERM 信号(终止信号),该信号用于终止一个进程。可以通过 kill -l 命令查看所有可用信号,kill -3 表示发送 SIGQUIT 信号。

JVM 源码分析(四):深入理解 park / unpark的更多相关文章

  1. JVM源码分析之Metaspace解密

        概述 metaspace,顾名思义,元数据空间,专门用来存元数据的,它是jdk8里特有的数据结构用来替代perm,这块空间很有自己的特点,前段时间公司这块的问题太多了,主要是因为升级了中间件所 ...

  2. JVM源码分析之synchronized实现

    “365篇原创计划”第十二篇.   今天呢!灯塔君跟大家讲:   JVM源码分析之synchronized实现     java内部锁synchronized的出现,为多线程的并发执行提供了一个稳定的 ...

  3. JVM源码分析之一个Java进程究竟能创建多少线程

    JVM源码分析之一个Java进程究竟能创建多少线程 原创: 寒泉子 你假笨 2016-12-06 概述 虽然这篇文章的标题打着JVM源码分析的旗号,不过本文不仅仅从JVM源码角度来分析,更多的来自于L ...

  4. JVM源码分析之堆外内存完全解读

    JVM源码分析之堆外内存完全解读   寒泉子 2016-01-15 17:26:16 浏览6837 评论0 阿里技术协会 摘要: 概述 广义的堆外内存 说到堆外内存,那大家肯定想到堆内内存,这也是我们 ...

  5. JVM源码分析-JVM源码编译与调试

    要分析JVM的源码,结合资料直接阅读是一种方式,但是遇到一些想不通的场景,必须要结合调试,查看执行路径以及参数具体的值,才能搞得明白.所以我们先来把JVM的源码进行编译,并能够使用GDB进行调试. 编 ...

  6. JVM源码分析之警惕存在内存泄漏风险的FinalReference(增强版)

    概述 JAVA对象引用体系除了强引用之外,出于对性能.可扩展性等方面考虑还特地实现了四种其他引用:SoftReference.WeakReference.PhantomReference.FinalR ...

  7. JVM源码分析之JVM启动流程

      原创申明:本文由公众号[猿灯塔]原创,转载请说明出处标注 “365篇原创计划”第十四篇. 今天呢!灯塔君跟大家讲: JVM源码分析之JVM启动流程 前言: 执行Java类的main方法,程序就能运 ...

  8. JVM源码分析之Object.wait/notify实现

    ​ “365篇原创计划”第十一篇.   今天呢!灯塔君跟大家讲:   JVM源码分析之Object.wait/notify实现       最简单的东西,往往包含了最复杂的实现,因为需要为上层的存在提 ...

  9. 学习JUC源码(3)——Condition等待队列(源码分析结合图文理解)

    前言 在Java多线程中的wait/notify通信模式结尾就已经介绍过,Java线程之间有两种种等待/通知模式,在那篇博文中是利用Object监视器的方法(wait(),notify().notif ...

  10. JVM源码分析之SystemGC完全解读

    JVM源码分析之SystemGC完全解读 概述 JVM的GC一般情况下是JVM本身根据一定的条件触发的,不过我们还是可以做一些人为的触发,比如通过jvmti做强制GC,通过System.gc触发,还可 ...

随机推荐

  1. AcWing 195. 骑士精神

    双向BFS (广搜) \(O(8 ^ 7)\) 看到没有双向BFS的题解我就过来了 这道题也可以用双向\(BFS\)来做,时间复杂度与\(IDA*\)不相上下. 双向\(BFS\)的实现有多种: 把初 ...

  2. 【学习笔记】浅析平衡树套线段树 & 带插入区间K小值

    常见的树套树 一般来说,在嵌套数据结构中,线段树多被作为外层结构使用. 但线段树毕竟是 静态 的结构,导致了一些不便. 下面是一个难以维护的例子: 带插入区间 \(k\) 小值问题 来源:Luogu ...

  3. 算法——模拟LRU机制

    运用你所掌握的数据结构,设计和实现一个 LRU (最近最少使用) 缓存机制 . 实现 LRUCache 类: LRUCache(int capacity) 以正整数作为容量 capacity 初始化 ...

  4. Struts2 S2-061(CVE-2020-17530)漏洞复现

    0x00 漏洞简介 Apache Struts2框架是一个用于开发Java EE网络应用程序的Web框架.Apache Struts于2020年12月08日披露 S2-061 Struts 远程代码执 ...

  5. react第十三单元(react路由-react路由的跳转以及路由信息) #课程目标

    第十三单元(react路由-react路由的跳转以及路由信息) #课程目标 熟悉掌握路由的配置 熟悉掌握跳转路由的方式 熟悉掌握路由跳转传参的方式 可以根据对其的理解封装一个类似Vue的router- ...

  6. [日常摸鱼]luogu1613跑路

    新年A的第一道题2333 https://www.luogu.org/problemnew/show/P1613 题意:给一张有向图,每条边长为1,每个单位时间只能走$2^k$的长度,$k$可以任意选 ...

  7. Java进阶:基于TCP通信的网络实时聊天室

    目录 开门见山 一.数据结构Map 二.保证线程安全 三.群聊核心方法 四.聊天室具体设计 0.用户登录服务器 1.查看当前上线用户 2.群聊 3.私信 4.退出当前聊天状态 5.离线 6.查看帮助 ...

  8. Springboot接入RabbitMQ详细教程

    本文适用于对 RabbitMQ 有所了解的人,在此不讨论MQ的原理,只讨论如何接入.其实Spring Boot 集成 RabbitMQ 非常简单,本文章使用的是Spring Boot 提供了sprin ...

  9. HTC Vive使用WebVR的方法以及启用后头显无画面的解决方法

    1.下载支持WebVR的浏览器. 笔者使用的是HTC Vive,故下载了Firefox的Nightly版本[下载地址]. 2.Nightly 开启WebVR的步骤[引用自Mozilla VR] 1.从 ...

  10. 【windows】【消息中间件】【安装】Elasticsearch

    一.准备工作 elasticsearch的下载地址:https://www.elastic.co/cn/downloads/elasticsearch ik分词器的下载地址:https://githu ...