前言

在java编程中,线程Thread是我们经常使用的类。那么创建一个Thread的本质究竟是什么,本文就此问题作一个探索。

内容主要分为以下几个部分

1.JNI机制的使用

2.Thread创建线程的底层调用分析

3.系统线程的使用

4.Thread中run方法的回调分析

5.实现一个jni的回调

1.JNI机制的基本使用

当我们new出一个Thread的时候,仅仅是创建了一个java层面的线程对象,而只有当Thread的start方法被调用的时候,一个线程才真正开始执行了。所以start方法是我们关注的目标

查看Thread类的start方法

public synchronized void start() {
if (threadStatus != 0)
throw new IllegalThreadStateException();
group.add(this); boolean started = false;
try {
start0();
started = true;
} finally {
try {
if (!started) {
group.threadStartFailed(this);
}
} catch (Throwable ignore) {
}
}
}

Start方法本身并不复杂,其核心是start0(),真正地将线程启动起来。

接着我们查看start0()方法

private native void start0();

可以看到这是一个native方法,这里我们需要先解释一下什么是native方法。

众所周知java是一个跨平台的语言,用java编译的代码可以运行在任何安装了jvm的系统上。然而各个系统的底层实现肯定是有区别的,为了使java可以跨平台,于是jvm提供了叫java native interface(JNI)的机制。当java需要使用到一些系统方法时,由jvm帮我们去调用系统底层,而java本身只需要告知jvm需要做的事情,即调用某个native方法即可。

例如,当我们需要启动一个线程时,无论在哪个平台上,我们调用的都是start0方法,由jvm根据不同的操作系统,去调用相应系统底层方法,帮我们真正地启动一个线程。因此这就像是jvm为我们提供了一个可以操作系统底层方法的接口,即JNI,java本地接口。

在深入查看start0()方法之前,我们先实现一个自己的JNI方法,这样才能更好地理解start0()方法是如何调用到系统层面的native方法。

首先我们先定义一个简单的java类

package cn.tera.jni;

public class JniTest {
public native void jniHello(); public static void main(String[] args) {
JniTest jni = new JniTest();
jni.jniHello();
}
}

在这个类中,我们定义了一个jniHello的native方法,然后在main方法中对其进行调用。

接着我们调用javac命令将其编译成一个class文件,但和平时不同,我们需要加一个-h参数,生成一个头文件

javac -h . JniTest.java

注意-h后面有一个.,意思是生成的头文件,存放在当前目录

这时我们可以看到在当前目录下生成了2个新文件

JniTest.class:JniTest类的字节码

cn_tera_jni_JniTest.h:.h头文件,这个文件是C和C++中所需要用到的,其中定义了方法的参数、返回类型等,但不包含实现,类似java中的接口,而java代码正是通过这个“接口”找到真正需要执行的方法。

我们查看该.h文件,其中就包含了jniHello方法的定义,当然需要注意到的是,这里的方法名和.h文件本身的命名是jni根据我们类的包名和类名确定出来的,不能修改。

/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class cn_tera_jni_JniTest */ #ifndef _Included_cn_tera_jni_JniTest
#define _Included_cn_tera_jni_JniTest
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: cn_tera_jni_JniTest
* Method: jniHello
* Signature: ()V
*/
JNIEXPORT void JNICALL Java_cn_tera_jni_JniTest_jniHello
(JNIEnv *, jobject); #ifdef __cplusplus
}
#endif
#endif

既然我们有了.h头文件,那么自然需要.c或者.cpp的定义实际执行内容的文件,即接口的实现。

我们希望该方法简单地输出一个"hello jni",于是定义如下方法,并将其保存在cn_tera_jni_JniTest.c文件中(这里文件名不需要一致,不过为了可维护性,我们应当定义一致)

#include "cn_tera_jni_JniTest.h"

JNIEXPORT void JNICALL Java_cn_tera_jni_JniTest_jniHello(JNIEnv *env, jobject c1){
printf("hello jni\n");
}

在该文件中,引入了之前生成.h文件(类似于java指定了类实现了哪个接口),并且定义了签名完全一致的Java_cn_tera_jni_JniTest_jniHello方法,此时我们已经有了“接口”和“实现”,接着生成动态链接库即可。

Mac系统运行命令:

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

Linux系统运行命令:

gcc -shared -I /usr/lib/jdk1.8.0_241/include cn_tera_jni_JniTest.c -o libJniTest.so

-dynamiclib、-shared:表示我们需要生成一个动态链接库

-I:之前在.h头文件中我们需要引入jni.h,而该文件位与jdk的目录下,这里-I就是include的意思

-o:表示输出的文件

​ 在Mac系统下,链接库的扩展名为jnilib,命名的格式为libXXX.jnilib

​ 在Linux系统下,链接库扩展名为so,命名格式为libXXX.so

​ 其中的XXX是在运行时加载动态库时用到的名字

此时在目录下就会多出一个libJniTest.jnilib或者libJniTest.so的动态链接库。

最后我们回到一开始的java文件中,引入该库即可。修改JniTest.java

package cn.tera.jni;

public class JniTest {
static {
//设置查找路径为当前项目路径
System.setProperty("java.library.path", ".");
//加载动态库的名称
System.loadLibrary("JniTest");
} public native void jniHello(); public static void main(String[] args) {
JniTest jni = new JniTest();
jni.jniHello();
}
}

重新编译.class文件,记得将其放到./cn/tera/jni目录下(包名是啥,目录就是啥),然后执行即可。

java cn.tera.jni.JniTest
hello jni

此时我们先总结一下JNI的基本使用顺序

1)在.java文件中定义native方法

2)生成相应的.h头文件(即接口)

3)编写相应的.c或.cpp文件(即实现)

4)将接口和实现链接到一起,生成动态链接库

5)在.java中引入该库,即可调用native方法

2.Thread创建线程的底层调用分析

了解了jni的基本使用流程之后,我们回到Thread的start0方法

为了探究start0()方法的原理,自然需要看看jvm在幕后为我们做了什么。

首先我们需要下载jdk和jvm的源码,因为openjdk和oraclejdk差别很小,而openjdk是开源的,所以我们以openjdk的代码为参考,版本是jdk8

下载地址:http://hg.openjdk.java.net/jdk8

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

在jdk源码的目录src/java.base/share/native/libjava目录下能看到Thread.c文件,对应的是jni中的“实现”

#include "jni.h"
#include "jvm.h" #include "java_lang_Thread.h"
...
static JNINativeMethod methods[] = {
{"start0", "()V", (void *)&JVM_StartThread},
...
};
JNIEXPORT void JNICALL
Java_java_lang_Thread_registerNatives(JNIEnv *env, jclass cls)
{
(*env)->RegisterNatives(env, cls, methods, ARRAY_LENGTH(methods));
}

按照之前我们自己定义的jni实现,该文件中应当有一个Java_java_lang_Thread_start0的方法定义,然而其中实际上只有一个Java_java_lang_Thread_registerNatives的方法定义,对应的正是Thread.java中的registerNatives方法:

class Thread implements Runnable {
private static native void registerNatives();
static {
registerNatives();
}
...
}

由此我们可以发现,Thread类在实现jni的时候并非是将每一个native方法都直接定义在自己的头文件中,而是通过一个registerNatives方法动态注册的,而注册所需要的信息都被定义在了methods数组中,包括方法名、方法签名和接口方法,接口方法的定义被统一放到了jvm.h中(#include "jvm.h")。这个时候该jni接口方法的名字就不再受到固定格式限制了。这个机制以后用单独的文章来解释,现在先关心Thread的本质。

接下去我会按照调用链从上至下的顺序列出文件和方法

1)jvm.h,hotspot目录src/share/vm/prims

既然start0方法的接口方法被定义在jvm.h中,那么我们先查看jvm.h,就可以找到JVM_StartThread的定义了:

JNIEXPORT void JNICALL
JVM_StartThread(JNIEnv *env, jobject thread);

2)jvm.cpp,hotspot目录src/share/vm/prims

接着我们查看jvm.cpp,这里能看到JVM_StartThread的具体实现,关键点是通过创建一个JavaThread类创建线程,注意这里JavaThread是C++级别的线程:

JVM_ENTRY(void, JVM_StartThread(JNIEnv* env, jobject jthread))
JVMWrapper("JVM_StartThread");
JavaThread *native_thread = NULL;
bool throw_illegal_thread_state = false; {
...
/**
* 创建一个C++级别的线程
*/
native_thread = new JavaThread(&thread_entry, sz);
...
}
...
JVM_END

3)thread.cpp,hotspot目录src/share/vm/runtime

查看thread.cpp,可以看到JavaThread的构造函数,其中创建了一个系统线程:

JavaThread::JavaThread(ThreadFunction entry_point, size_t stack_sz) :
Thread()
{
...
/**
* 创建系统线程
*/
os::create_thread(this, thr_type, stack_sz);
}

4)os_linux.cpp,hotspot目录src/os/linux/vm

我们能在hotspot源码目录的src/os下找到不同系统的方法,我们以linux系统为例。

查看os_linux.cpp,找到create_thread方法:

bool os::create_thread(Thread* thread, ThreadType thr_type,
size_t req_stack_size) {
...
pthread_t tid;
int ret = pthread_create(&tid, &attr, (void* (*)(void*)) thread_native_entry, thread);
...
}

这个pthread_create方法就是最终创建系统线程的底层方法

因此java线程start方法的本质其实就是通过jni机制,最终调用系统底层的pthread_create方法,创建了一个系统线程,因此java线程和系统线程是一个一对一的关系

3.系统线程的使用

接着我们来简单使用一下这个创建线程的方法。创建如下的.c文件,在main方法中创建一个线程,并让2个线程不断打印一些文案

#include <pthread.h>
#include <stdio.h> pthread_t pid; void* thread_entity(void* arg){
while (1) {
printf("i am thread\n");
}
} int main(){
pthread_create(&pid,NULL,thread_entity,NULL);
while (1) {
printf("i am main\n");
}
return 1;
}

编译该文件

gcc threaddemo.c -o threaddemo.out

-o:编译后的执行文件为threaddemo.out

运行该out文件后就能看到2个文案在不断重复打印了,也就是成功通过pthread_create方法创建了一个系统级别的线程。

4.Thread中run方法的回调分析

到这里我们的探究并没有结束,在java的Thread类中,我们会传入一个执行我们指定任务的Runnable对象,在Thread的run()方法中调用。当java通过jni调用到pthread_create创建完系统线程后,又要如何回调java中的run方法呢?

前面的探究我们是从java层开始,从上往下找,此时我们要反过来,从下往上找了。

1)pthread_create

先看pthread_create方法本身,它接收4个参数,其中第三个参数start_routine是系统线程创建后需要执行的方法,就像前面我们创建的简单示例中的thread_entity,而第四个参数argstart_routine方法需要的参数

pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine)(void *), void *arg);

2)os_linux.cpp

查看create_thread方法中调用pthread_create的代码,可以看到thread_native_entry就是系统线程所执行的方法,而thread则是传递给thread_native_entry的参数:

int ret = pthread_create(&tid, &attr, (void* (*)(void*)) thread_native_entry, thread);

查看thread_native_entry方法,它获取的参数正是一个Thread,并调用其run()方法。注意这个Thread是C++级别的线程,来自于pthread_create方法的第4个参数:

static void *thread_native_entry(Thread *thread) {
...
// call one more level start routine
thread->run();
...
return 0;
}

3)thread.cpp

查看JavaThread::run()方法,其主要的执行内容在thread_main_inner方法中:

void JavaThread::run() {
/**
* 主要的执行内容
*/
thread_main_inner();
}

查看JavaThread::thread_main_inner()方法,其内部通过entry_point执行回调:

void JavaThread::thread_main_inner() {
...
/**
* 调用entry_point,执行外部传入的方法,注意这里的第一个参数是this
* 即JavaThread对象本身,后面会看到该方法的定义
*/
this->entry_point()(this, this);
...
}

查看JavaThread::JavaThread构造函数,可以看到这里的entry_point是从外部传入的

JavaThread::JavaThread(ThreadFunction entry_point, size_t stack_sz) :
Thread()
{
...
set_entry_point(entry_point);
...
}

4)jvm.cpp

查看JVM_StartThread方法,可以看到传给JavaThread的entry_pointthread_entry

JVM_ENTRY(void, JVM_StartThread(JNIEnv* env, jobject jthread))
JVMWrapper("JVM_StartThread");
JavaThread *native_thread = NULL;
bool throw_illegal_thread_state = false; {
...
/**
* 传给构造函数的entry_point是thread_entry
*/
native_thread = new JavaThread(&thread_entry, sz);
...
}
...
JVM_END

查看thread_entry,其中调用了JavaCalls::call_virtual去回调java级别的方法,其实看到它的方法签名就能猜到个大概了

static void thread_entry(JavaThread* thread, TRAPS) {
HandleMark hm(THREAD);
/**
* obj正是根据thread对象获取到的,JavaThread在调用时会传入this
*/
Handle obj(THREAD, thread->threadObj());
/**
* 返回结果是void
*/
JavaValue result(T_VOID);
/**
* 回调java级别的方法
*/
JavaCalls::call_virtual(&result,//返回对象
//实例对象
obj,
//类
KlassHandle(THREAD, SystemDictionary::Thread_klass()),
//方法名
vmSymbols::run_method_name(),
//方法签名
vmSymbols::void_method_signature(),
THREAD);
}

5)vmSymbols.hpp,hotspot目录src/share/vm/classfiles

我们查看获取方法名run_method_name和方法签名void_method_signature的部分,可以看到正是获取一个方法名为run,且不获取任何参数,返回值为void的方法:

template(run_method_name,                           "run")
...
template(void_method_signature, "()V")

于是系统线程就能成功地回调java级别的run方法了!

这里我整理了一下Thread的start0方法的调用上下游关系,方便大家整体把握

Thread.java

-------->jvm.cpp

​ -------->thread.cpp

​ -------->os_linux.cpp

​ -------->pthread_create

5.实现一个jni的回调

最后我们尝试自己实现一个简单的方法回调。

修改一开始的JniTest.java,新增一个回调方法:

package cn.tera.jni;

public class JniTest {
static {
//设置查找路径为当前项目路径
System.setProperty("java.library.path", ".");
//加载动态库的名称
System.loadLibrary("JniTest");
} public native void jniHello(); //新增一个回调方法
public void callBack(){
System.out.println("this is call back");
} public static void main(String[] args) {
JniTest jni = new JniTest();
jni.jniHello();
}
}

修改cn_tera_jni_JniTest.c文件,原先只是简单输出一个文案,现在改为回调java方法。可以看到这个流程和java中的反射机制非常相似:

#include "cn_tera_jni_JniTest.h"

JNIEXPORT void JNICALL Java_cn_tera_jni_JniTest_jniHello(JNIEnv *env, jobject c1){
//获取类信息
jclass thisClass = (*env)->GetObjectClass(env, c1);
//根据方法名和签名获取方法的id
jmethodID midCallBack = (*env)->GetMethodID(env, thisClass, "callback", "()V");
//调用方法
(*env)->CallVoidMethod(env, c1, midCallBack);
}

重新生成动态链接库、编译.class文件、运行:

gcc -dynamiclib -I /Library/Java/JavaVirtualMachines/jdk1.8.0_241.jdk/Contents/Home/include cn_tera_jni_JniTest.c -o libJniTest.jnilib
javac JniTest.java
java cn.tera.jni.JniTest

成功得到输出结果:

this is call back

当然,对于有参数的、有返回结果的回调等,jni也提供了不同的调用方法,这个就不在本文中展开了,有兴趣的同学可以自己去看下jni.h文件

还要提一点,上面展示的回调只是最基本的使用,而jvm中的官方回调方法,因为涉及到了java的父类继承关系、方法句柄、vtable等等内容,这里也就不展开了,同学们自己研究吧

最后,总结一下本文的内容

1.实现一个jni只需要4个东西,.java文件,.h头文件(相当于接口),.c或.cpp文件(相当于实现),生成的动态链接库。

2.java的Thread是通过jni机制最终调用到了系统底层的pthread_create方法创建线程的。

3.Thread的jni调用链:Thread.java->jvm.cpp->thread.cpp->os_linux.cpp->pthread_create

4.jni也可以回调java方法,从调用到回调完成了一个demo

JNI-Thread中start方法的调用与run方法的回调分析的更多相关文章

  1. javascript中onclick事件能调用多个方法吗

    Q: javascript中onclick事件能调用多个方法吗? A: 可以的,方法如下onclick="aa();bb();cc();"每个方法用“;”分号隔开就行了

  2. QT源码解析(七)Qt创建窗体的过程,作者“ tingsking18 ”(真正的创建QPushButton是在show()方法中,show()方法又调用了setVisible方法)

    前言:分析Qt的代码也有一段时间了,以前在进行QT源码解析的时候总是使用ue,一个函数名在QTDIR/src目录下反复的查找,然后分析函数之间的调用关系,效率实在是太低了,最近总结出一个更简便的方法, ...

  3. SNF快速开发平台MVC-EasyUI3.9之-WebApi和MVC-controller层接收的json字符串的取值方法和调用后台服务方法

    最近项目组很多人问我,从前台页面传到后台controller控制层或者WebApi 时如何取值和运算操作. 今天就都大家一个在框架内一个取值技巧 前台JS调用代码: 1.下面是选中一行数据后右键点击时 ...

  4. 是否可以从一个static方法内部调用非static方法?

    不可以.静态成员不能调用非静态成员. 非static方法属于对象,必须创建一个对象后,才可以在通过该对象来调用static方法.而static方法调用时不需要创建对象,通过类就可以调用该方法.也就是说 ...

  5. Java中多线程启动,为什么调用的是start方法,而不是run方法?

    前言 大年初二,大家新年快乐,我又开始码字了.写这篇文章,源于在家和基友交流的时候,基友问到了,我猛然发现还真是这么回事,多线程启动调用的都是start,那么为什么没人掉用run呢?于是打开我的ide ...

  6. python__基础 : 多继承中方法的调用顺序 __mro__方法

    在多继承中,如果一个子类继承了两个平级的父类,而这两个父类有两个相同名字的方法,那么一般先继承谁,调用方法就调用先继承的那个父类的方法.如: class A: def test(self): prin ...

  7. 10、一个action中处理多个方法的调用第二种方法method的方式

    在实际的项目中,经常采用现在的第二种方式在struct.xml中采用清单文件的方式 我们首先来看action package com.bjpowernode.struts2; import com.o ...

  8. 10、一个action中处理多个方法的调用第一种方法动态调用

    我们新建一个用户的action package com.weiyuan.test; import com.opensymphony.xwork2.ActionSupport; /** * * 这里不用 ...

  9. Spring中 PROPAGATION_REQUIRED 解释 事物是在一个方法里调用其他的方法,一起成功或者一起失败,是方法之间的关系,而不是某一个方法内部的问题。而且要以抛异常的方式来表明方法的失败,以此来导致事物起作用,大家全失败。

    事务传播行为种类 Spring在TransactionDefinition接口中规定了7种类型的事务传播行为, 它们规定了事务方法和事务方法发生嵌套调用时事务如何进行传播: 事务传播行为类型 事务传播 ...

随机推荐

  1. 0923 lca练习

    P1967 货车运输 题目描述 A 国有 nnn 座城市,编号从 11 1 到 n nn,城市之间有 mmm 条双向道路.每一条道路对车辆都有重量限制,简称限重. 现在有 qqq 辆货车在运输货物, ...

  2. 安装zabbix3.0以及升级到5.0过程

    关闭防火墙: systemctl stop firewalld.service systemctl disable firewalld.service 需要关闭 selinux,一定要关闭这个,开启s ...

  3. 6个LED的控制

    控制任务和要求 让6个LED按要求工作 电路设计 程序设计 1 int Led1 = 1; //各LED与实验板的联接引脚 2 int Led2 = 2; 3 int Led3 = 3; 4 int ...

  4. linux下的echo

    echo命令用于在shell中打印shell变量的值,或者直接输出指定的字符串.linux的echo命令,在shell编程中极为常用, 在终端下打印变量value的时候也是常常用到的,因此有必要了解下 ...

  5. IDEA2020.2的破解

    第一种方式:http://code.39sd.cn/ 直接获取二维码: 第二种:下载破解工具(本方法只是提供个人学习使用) 1.下载2020.2的idea 链接:https://pan.baidu.c ...

  6. redis哨兵搭建

    redis哨兵搭建 1.复制配置文件到conf #单机安装以后[root@t3 redis-5.0.8]# pwd/app/redis-5.0.8[root@t3 redis-5.0.8]# cp s ...

  7. 在Linux命令行内的大小写转换

    在编辑文本时大小写常常是需要注意的地方,大小写的转换是很枯燥而繁琐的工作,所幸,Linux 提供了很多能让这份工作变得容易的命令.接下来让我们看看都有哪些完成大小写转换的命令. tr 命令 tr (t ...

  8. centos8平台用ss监控网络

    一,ss所属的包: [root@blog ~]# whereis ss ss: /usr/sbin/ss /usr/share/man/man8/ss.8.gz [root@blog ~]# rpm ...

  9. Vue3 来了,Vue3 开源商城项目重构计划正式启动!

    我打算用 Vue3 写一个商城项目,目前已经开始着手开发,测试完成后正式开源到 GitHub,让大家也可以用现成的 Vue3 大型商城项目源码来练练手. Vue 3.0 来了,我们该做些什么? Vue ...

  10. Anderson《空气动力学基础》5th读书笔记 第0记——白金汉PI定理

    目录 量纲分析:白金汉PI定理 相似参数 量纲分析:白金汉PI定理 在空气动力学中,飞机的空气动力主要由自由来流的密度ρ∞,自由来流数V∞,翼弦长度c,自由来流的粘性系数μ∞以及音速a∞,所以假设我们 ...