分析JVM源码之Thread.interrupt系统级别线程打断


Posted in Java/Android onJune 29, 2021
目录
  • 一、interrupt的使用特点
  • 二、jvm层面上interrupt方法的本质
  • 三、ParkEvent对象的本质
  • 四、Park()对象的本质
  • 五、利用jni实现一个可以被打断的MyThread类
  • 六、总结

http://hg.openjdk.java.net/jdk8

因为C和C++的代码对于java程序员来说比较晦涩难懂,所以在下方展示源码的时候我只会贴出我们关心的重点代码,其余的部分就省略了。

查看Thread.c:jdk源码目录src/java.base/share/native/libjava

找到如下代码:

static JNINativeMethod methods[] = {
    ...
    {"interrupt0",       "()V",        (void *)&JVM_Interrupt}
    ...
};

可以看到interrupt0对应的jvm方法是JVM_Interrupt

查看jvm.cpp,hotspot目录src/share/vm/prims

可以找到JVM_Interrupt方法的实现,这个方法挺简单的:

JVM_ENTRY(void, JVM_Interrupt(JNIEnv* env, jobject jthread))
  JVMWrapper("JVM_Interrupt");
  ...
  if (thr != NULL) {
    //执行线程打断操作
    Thread::interrupt(thr);
  }
JVM_END

查看thread.cpp,hotspot目录src/share/vm/runtime

找到interrupt方法:

void Thread::interrupt(Thread* thread) {
  //执行os层面的打断
  os::interrupt(thread);
}

查看os_posix.cpp,hotspot目录src/os/posix/vm

找到interrupt方法,这个方法正是打断的重点:

void os::interrupt(Thread* thread) {
  ...
  //获得c++线程对应的系统线程
  OSThread* osthread = thread->osthread();
  //如果系统线程的打断标记是false,意味着还未被打断
  if (!osthread->interrupted()) {
    //将系统线程的打断标记设为true
    osthread->set_interrupted(true);
    //这个涉及到内存屏障,本文不展开
    OrderAccess::fence();
    //这里获取一个_SleepEvent,并调用其unpark()方法
    ParkEvent * const slp = thread->_SleepEvent ;
    if (slp != NULL) slp->unpark() ;
  }

  //这里依据JSR166标准,即使打断标记为true,依然要调用下面的2个unpark
  if (thread->is_Java_thread())
    //如果是一个java线程,这里获取一个parker对象,并调用其unpark()方法
    ((JavaThread*)thread)->parker()->unpark();

  ParkEvent * ev = thread->_ParkEvent ;
  //这里获取一个_ParkEvent,并调用其unpark()方法
  if (ev != NULL) ev->unpark() ;
}

这个方法中,首先判断线程的打断标志,如果为false,则将其设置为true

并且调用了3个对象的unpark()方法,一会儿介绍着3个对象的作用。

总而言之,线程打断的本质做了2件事情

1.将线程的打断标志设置为true

2.调用3个对象的unpark方法唤醒线程

三、ParkEvent对象的本质

在前面我们看到线程在调用interrupt方法的最底层其实是调用了thread中3个对象的unpark()方法,那么这3个对象究竟代表了什么呢,我们继续探究。

首先我们先看SleepEvent和ParkEvent对象,这2个对象的类型是相同的

查看thread.cpp,hotspot目录src/share/vm/runtime

找到SleepEvent和ParkEvent的定义,jvm已经给我们注释了,ParkEven是供synchronized()使用,SleepEvent是供Thread.sleep使用:

ParkEvent * _ParkEvent;    // for synchronized()
ParkEvent * _SleepEvent;   // for Thread.sleep

查看park.hpp,hotspot目录src/share/vm/runtime

在头文件中能找到ParkEvent类的定义,继承自os::PlatformEvent,是一个和系统相关的的PlatformEvent:

class ParkEvent : public os::PlatformEvent {
  ...
}

查看os_linux.hpp,hotspot目录src/os/linux/vm

以linux系统为例,在头文件中可以看到PlatformEvent的具体定义,我们只关注其中的重点:

首先是2个私有对象,一个pthread_mutex_t操作系统级别的信号量,一个pthread_cond_t操作系统级别的条件变量,这2个变量是一个数组,长度都是1,这些在后面会看到是如何使用的

其次是定义了3个方法,park()、unpark()、park(jlong millis),控制线程的挂起和继续执行

class PlatformEvent : public CHeapObj<mtInternal> {
 private:
  ...
  pthread_mutex_t _mutex[1];
  pthread_cond_t  _cond[1];
  ...
  void park();
  void unpark();
  int  park(jlong millis); // relative timed-wait only
  ...
};

查看os_linux.cpp,hotspot目录src/os/linux/vm

接着我们就需要去看park和unpark方法的具体实现,并看看2个私有变量是如何被使用的

先看park()方法,这里我们主要关注3个系统底层方法的调用

pthread_mutex_lock(_mutex):锁住信号量

status = pthread_cond_wait(_cond, _mutex):释放信号量,并在条件变量上等待

status = pthread_mutex_unlock(_mutex):释放信号量

void os::PlatformEvent::park() { 
    ...
    //锁住信号量
    int status = pthread_mutex_lock(_mutex);
    while (_Event < 0) {
      //释放信号量,并在条件变量上等待
      status = pthread_cond_wait(_cond, _mutex);
    }
    //释放信号量
    status = pthread_mutex_unlock(_mutex);
}

这个方法其实非常好理解,就相当于:

synchronize(obj){
  obj.wait();
}

或者:

ReentrantLock lock = new ReentrantLock();
Condition condition = lock.newCondition();
lock.lock();
condition.wait();
lock.unlock();

park(jlong millis)方法就不展示了,区别只是调用一个接受时间参数的等待方法。

所以park()方法底层其实是调用系统层面的锁和条件等待去挂起线程的

接着我们看unpark()方法,其中最重要的方法当然是

pthread_cond_signal(_cond):唤醒条件变量

void os::PlatformEvent::unpark() {
  ...
  if (AnyWaiters != 0) {
    //唤醒条件变量
    status = pthread_cond_signal(_cond);
  }
  ...
}

所以unpark()方法底层其实是调用系统层面的唤醒条件变量达到唤醒线程的目的

四、Park()对象的本质

看完了2个ParkEvent对象的本质,那么接着我们还剩一个park()对象

查看thread.hpp,hotspot目录src/share/vm/runtime

park()对象的定义如下:

public:
  Parker*     parker() { return _parker; }

查看park.hpp,hotspot目录src/share/vm/runtime

可以看到,它是继承自os::PlatformParker,和ParkEvent不同,下面可以看到,等待变量的数组长度变为了2,其中一个给相对时间使用,一个给绝对时间使用

class Parker : public os::PlatformParker {
    pthread_mutex_t _mutex[1];
    pthread_cond_t  _cond[2]; // one for relative times and one for abs.
}

查看os_linux.cpp,hotspot目录src/os/linux/vm

还是先看park方法的实现,这个方法其实是对ParkEvent中的park方法的改良版,不过总体的逻辑还是没有变

最终还是调用pthread_cond_wait方法挂起线程

void Parker::park(bool isAbsolute, jlong time) {
  ...
  if (time == 0) {
    //这里是直接长时间等待
    _cur_index = REL_INDEX; 
    status = pthread_cond_wait(&_cond[_cur_index], _mutex);
  } else {
    //这里会根据时间是否是绝对时间,分别等待在不同的条件上
    _cur_index = isAbsolute ? ABS_INDEX : REL_INDEX;
    status = pthread_cond_timedwait(&_cond[_cur_index], _mutex, &absTime);
  }
  ...
}

最后看一下unpark方法,这里需要先获取一个正确的等待对象,然后通知即可:

void Parker::unpark() {
  int status = pthread_mutex_lock(_mutex);
  ...
  //因为在等待的时候会有2个等待对象,所以需要先获取正确的索引
  int index = _cur_index;
  ...
  status = pthread_mutex_unlock(_mutex);
  if (s < 1 && index != -1) {
    //唤醒线程
    status = pthread_cond_signal(&_cond[index]);
  }
  ...
}

五、利用jni实现一个可以被打断的MyThread类

结合上一篇文章,我们利用jni实现一个自己可以被打断的简易MyThread类

首先定义MyThread.java

import java.util.concurrent.TimeUnit;
import java.time.LocalDateTime;

public class MyThread {

    static {
        //设置查找路径为当前项目路径
        System.setProperty("java.library.path", ".");
        //加载动态库的名称
        System.loadLibrary("MyThread");
    }

    public native void startAndPark();

    public native void interrupt();

    public static void main(String[] args) throws InterruptedException {
        MyThread thread = new MyThread();
        //启动线程打印一段文字,并睡眠
        thread.startAndPark();
        //1秒后主线程打断子线程
        TimeUnit.MILLISECONDS.sleep(1000);
        System.out.println(LocalDateTime.now() + ":Main---准备打断线程");
        //打断子线程
        thread.interrupt();
        System.out.println(LocalDateTime.now() + ":Main---打断完成");
    }
}

执行命令编译MyThread.class文件并生成MyThread.h头文件

javac -h . MyThread.java

创建MyThread.c文件

当java代码调用startAndPark()方法的时候,创建了一个系统级别的线程,并调用pthread_cond_wait进行休眠

当java代码调用interrupt()方法的时候,会唤醒休眠中的线程

#include <pthread.h>
#include <stdio.h>
#include "MyThread.h"
#include "time.h"

pthread_t pid;
pthread_mutex_t _mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t  _cond = PTHREAD_COND_INITIALIZER; 

//打印时间
void printTime(){
    char strTm[50] = { 0 };
	  time_t currentTm;
	  time(&currentTm);
	  strftime(strTm, sizeof(strTm), "%x %X", localtime(&currentTm));
	  puts(strTm);
}

//子线程执行的方法
void* thread_entity(void* arg){
    printTime();
    printf("MyThread---启动\n");
    printTime();
    printf("MyThread---准备休眠\n");
    //阻塞线程,等待唤醒
    pthread_cond_wait(&_cond, &_mutex);
    printTime();
    printf("MyThread---休眠被打断\n");
}
//对应MyThread中的startAndPark方法
JNIEXPORT void JNICALL Java_MyThread_startAndPark(JNIEnv *env, jobject c1){
    //创建一个子线程
    pthread_create(&pid, NULL, thread_entity, NULL);
}
//对应MyThread中的interrupt方法
JNIEXPORT void JNICALL Java_MyThread_interrupt(JNIEnv *env, jobject c1){
    //唤醒线程
    pthread_cond_signal(&_cond);
}

执行命令创建动态链接库

gcc -dynamiclib -I /Library/Java/JavaVirtualMachines/jdk1.8.0_241.jdk/Contents/Home/include MyThread.c -o libMyThread.jnilib 

执行java的main方法,得到结果

子线程启动后进入睡眠,主线程1秒钟后打断子线程,完全符合我们的预期

2020/11/13 19时42分57秒

MyThread---启动

2020/11/13 19时42分57秒

MyThread---准备休眠

2020-11-13T19:42:58.891:Main---准备打断线程

2020/11/13 19时42分58秒

MyThread---休眠被打断

2020-11-13T19:42:58.891:Main---打断完成

六、总结

1.线程打断的本质做了2件事情:设置线程的打断标记,并调用线程3个Park对象的unpark()方法唤醒线程

2.线程挂起的本质是调用系统级别的pthread_cond_wait方法,使得等待在一个条件变量上

3.线程唤醒的本质是调用系统级别的pthread_cond_signal方法,唤醒等待的线程

4.通过实现一个自己的可以打断的线程类更好地理解线程打断的本质

以上就是分析JVM源码之Thread.interrupt系统级别线程打断的详细内容,更多关于JVM Thread.interrupt 系统级别线程打断的资料请关注三水点靠木其它相关文章!

Java/Android 相关文章推荐
详解Java实践之抽象工厂模式
Jun 18 Java/Android
总结一下关于在Java8中使用stream流踩过的一些坑
Jun 24 Java/Android
一篇文章带你复习java知识点
Jun 28 Java/Android
mybatis 解决从列名到属性名的自动映射失败问题
Jun 30 Java/Android
浅谈sql_@SelectProvider及使用注意说明
Aug 04 Java/Android
关于springboot配置druid数据源不生效问题(踩坑记)
Sep 25 Java/Android
聊聊SpringBoot自动装配的魔力
Nov 17 Java/Android
Java中API的使用方法详情
Apr 06 Java/Android
MyBatis XPathParser解析器使用范例详解
Jul 15 Java/Android
Spring Boot实现文件上传下载
Aug 14 Java/Android
JDK8中String的intern()方法实例详细解读
Sep 23 Java/Android
Java中的Kafka为什么性能这么快及4大核心详析
Sep 23 Java/Android
Jackson 反序列化时实现大小写不敏感设置
Jun 29 #Java/Android
Maven学习----Maven安装与环境变量配置教程
Spring Boot两种全局配置和两种注解的操作方法
Spring Boot 实现敏感词及特殊字符过滤处理
Jun 29 #Java/Android
elasticSearch-api的具体操作步骤讲解
Java SSH 秘钥连接mysql数据库的方法
一篇文章带你复习java知识点
You might like
Cakephp 执行主要流程
2010/03/24 PHP
浅谈php提交form表单
2015/07/01 PHP
基于jquery的不规则矩形的排列实现代码
2012/04/16 Javascript
JS隐藏参数post传值实例
2013/04/18 Javascript
网站如何做到完全不需要jQuery也可以满足简单需求
2013/06/27 Javascript
Javascript模块化编程详解
2014/12/01 Javascript
jQuery中Form相关知识汇总
2015/01/06 Javascript
JavaScript运动减速效果实例分析
2015/08/04 Javascript
js计算文本框输入的字符数
2015/10/23 Javascript
JavaScript函数中关于valueOf和toString的理解
2016/06/14 Javascript
Javascript 创建类并动态添加属性及方法的简单实现
2016/10/20 Javascript
浅谈JavaScript的闭包函数
2016/12/08 Javascript
react-native ListView下拉刷新上拉加载实现代码
2017/08/03 Javascript
简单的Vue异步组件实例Demo
2017/12/27 Javascript
Puppet的一些技巧
2018/09/17 Javascript
JS实现普通轮播图特效
2020/01/01 Javascript
[02:32]DOTA2亚洲邀请赛 C9战队出场宣传片
2015/02/07 DOTA
[59:15]EG vs LGD 2018国际邀请赛淘汰赛BO3 第一场 8.26
2018/08/29 DOTA
[48:27]EG vs Liquid 2018国际邀请赛淘汰赛BO3 第二场 8.25
2018/08/29 DOTA
python石头剪刀布小游戏(三局两胜制)
2021/01/20 Python
详解python算法之冒泡排序
2019/03/05 Python
Python玩转加密的技巧【推荐】
2019/05/13 Python
Tensorflow轻松实现XOR运算的方式
2020/02/03 Python
windows10 pycharm下安装pyltp库和加载模型实现语义角色标注的示例代码
2020/05/07 Python
关于css兼容性问题及一些常见问题汇总
2016/05/03 HTML / CSS
HTML5事件方法全部汇总
2016/05/12 HTML / CSS
在canvas上实现元素图片镜像翻转动画效果的方法
2018/03/20 HTML / CSS
英国内衣连锁店:Boux Avenue
2018/01/24 全球购物
酒店出纳岗位职责
2013/12/29 职场文书
学习方法演讲稿
2014/05/10 职场文书
校园绿化美化方案
2014/06/08 职场文书
纪念九一八事变演讲稿:牢记九一八,屈辱怎能忘
2014/09/14 职场文书
2014年数学教研组工作总结
2014/12/06 职场文书
2015年试用期自我评价范文
2015/03/10 职场文书
导游词之湖州-太湖
2019/10/11 职场文书
Vue h函数的使用详解
2022/02/18 Vue.js