基于JNI手动模拟Java线程
并发一直是个难点,也是面试的重灾区,闲来无事,就想知道Java的线程如何启动起来的。我们都知道Java的线程和系统的线程是一一对应的。要想知道Java的线程是如何启动起来,我们要先知道Java的线程的创建的方式。Java线程的创建方式。继承Thread类实现Runnable接口我们都知道Java线程创建方式有上面两种方式,但是还是没有谈论到我们的重点,Java的线程是如何启动起来,于是我要去查看s
并发一直是个难点,也是面试的重灾区,闲来无事,就想知道Java的线程如何启动起来的。我们都知道Java的线程和系统的线程是一一对应的。要想知道Java的线程是如何启动起来,我们要先知道Java的线程的创建的方式。
Java线程的创建方式。
继承Thread类
实现Runnable接口
我们都知道Java线程创建方式有上面两种方式,但是还是没有谈论到我们的重点,Java的线程是如何启动起来,于是我要去查看start方法的源码。
当我们点开源码发现start()方法中调用了start0()方法,而start0()方法是调用本地方法,这样需要去看JDK的源码,难道就到这了吗?当然不是,我们需要深入,于是我下载了JDK的源码,去找这个start0()的本地方法。注意:本人用的jdk8的源码,如果源码不一样可能目录结构不一样。
下面的文件的路径是:
你的源码的目录\jdk\src\share\native\java\lang\Thread.c中的第44行
在我很不懈的努力下终于找到了这个本地方法,但是又关联了JVM_StartThread()方法,于是我又去Hotspot源码中找这个方法,看Hotspot调用Linux底层的源码。
下面文件的目录是:
你的源码的目录\hotspot\src\share\vm\prims\jvm.cpp中的第2816行
当我打开这个文件的时候,我的内心是崩溃的。因为没有编译JDK的源码,所以无法继续下去,难道就到这了,于是我又去编译了JDK12的源码,参考的是周志明大神的《深入理解Java虚拟机》第三版(至于怎么编译在我的博客中有写到)
当打开编译好的JDK源码的时候,虽然能跳转了,但是我还是不知道要进入那个方法中去,内心还是崩溃的。有时候正推不行,我们就反推嘛!我们刚开始猜测Java中的线程和操作系统中线程是一一对应的。于是我搜索了一下,发现Linux中创建线程的方法是pthread_create,我们执行如下的命令:
man pthread_create
根据man配置的信息可以得到pthread_create会创建一个线程,这个函数式Linux系统的函数,可以用C或者C++直接调用。这个函数有四个参数。
参数 | 解释 | 备注 |
---|---|---|
pthread_t *thread | 传出参数,调用之后会传出被创建线程的id | 定义pthread_t pid;继而取地址 &pid |
const pthread_attr_t *attr | 线程属性,关于线程属性是Linux的知识 | 在学习pthread_create函数的时候一般传NULL,保持默认属性 |
void *(*start_routine)(void *) | 线程的启动后的主体函数相当于java当中的run | 需要你定义一个函数,然后传函数名即可 |
void *arg | 主体函数的参数 | 如果没有可以传NULL |
有了上面的知识,我们现在Linux上手撸一个线程。
首先我们在Linux桌面上创建一个文件夹Thread,然后在该目录下用vim创建了一个thread.c文件,具体的代码如下:
#include <pthread.h>//导入对应的头文件
#include <stdio.h>
pthread_t pid;//定义一个变量,接受创建线程后的线程id
//定义线程的主体函数
void* thread_entity(void* arg){
while(1){
//线程睡眠
usleep(100);
printf("I am new Thread\n");
}
}
//mian方法,程序入口,main和java的main一样会产生一个进程,继而产生一个main线程
int main(){
//调用操作系统的函数创建线程,注意四个参数
pthread_create(&pid,NULL,thread_entity,NULL);
while(1){
usleep(100);
printf("I am main\n");
}
}
当代码书写完成后,我们要开始进行编译,具体的命令如下:
gcc thread.c -o thread.out -pthread
编译会报一个warning,直接忽视,有些Linux版本是没有这个warning的。
最后我们开始执行运行的命令:
./thread.out
执行的结果如下:
两个线程会交替的执行,这样我们在Linux上实现了一个简单的多线程程序。
有了这些基础,现在回到我们Java上面来,我们的猜测Java的线程创建是调用了系统上创建线程函数。那我们该如何验证我们的猜想呢?我们可以创建一个Java的线程,然后找到JDK源码中有没有对应调用系统中的创建线程的函数,在该函数上面打一个断点,利用Clion IDE调试看看会不会调用到对应的函数。
我们先写一个Java多线程程序,然后将写好的Java程序放到我们的编译好的JDK源码下面build/linux-x86_64-server-fastdebug/jdk/bin/路径下,可能每个人编译路径路径不一样,你要找到你对应的文件下面,不然调试不行。具体怎么调试,在我的博客《Ubuntu18.04下编译JDK12》中有。
书写的Java代码如下:
public class Test{
public static void main(String[] args){
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("haha");
}
});
thread.start();
}
}
然后执行javac命令将Test.java文件编译成Test.class文件,最后我们在Clion中进行调试。调试之前,要进行对应的调试的配置。
当断点和调试配置弄好过后,我们可以开始debug了,来验证我们的猜想。
很高兴,我们的猜想是正确的。所以Java线程的创建会调用系统中的线程创建函数。
既然我们已经知道了Java的线程是怎么创建启动起来的,那我们为什么不去模拟一个Thread类,让Linux系统来调用我们写的Thread类中run方法,这样我们就能大致的模拟出Java原生Thread类的功能,我们下面要用的是Java中JNI技术,不清楚的可以看看《Java核心技术卷2》最后一章。
首先我们先创建一个MyThread.java,具体代码如下:
public class MyThread {
static {
//装载自己的库
System.loadLibrary("MyThreadNative");
}
public static void main(String[] args) {
MyThread myThread = new MyThread();
myThread.start0();
}
private native void start0();
}
然后进行编译javac MyThread.java,生成MyThread.class文件
再生成对应的.h的头文件,执行javah MyThread生成MyThread.h文件
然后在新建一个MyThread.c文件,具体的代码如下:
#include <pthread.h>
#include <stdio.h>
#include "MyThread.h"//记得导入刚刚编译的那个.h文件
pthread_t pid;
void* thread_entity(void* arg){
while(1){
usleep(100);
printf("I am new Thread\n");
}
}
//这个方法要参考.h文件的15行代码,这里的参数得注意,你写死就行,不用明白为什么
JNIEXPORT void JNICALL Java_MyThread_start0(JNIEnv *env, jobject c1){
pthread_create(&pid,NULL,thread_entity,NULL);
while(1){
usleep(100);
printf("I am main\n");
}
}
int main(){
return 0;
}
然后就解析类,把这个MyThread.c编译成为一个动态链接库,这样在java代码里会被load到内存libMyThreadNative这个命名需要注意的libxx,xx这里等于你java那边写的字符串,具体指令如下:
gcc -fPIC -I /usr/lib/jvm/java‐1.8.0‐openjdk-amd64/include -I /usr/lib/jvm/java‐1.8.0‐openjdk-amd64/include/linux -shared -o libMyThreadNative.so MyThread.c
上面的warning不用管,接下来将我们这个.so文件加入到path,这样java才能load的到,具体指令如下:
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:{libMyThreadNative.so}所在的路径
最后执行,发现已经成功了,但是并不是调用我们Java类中的run方法,这时候我们继续使用JNI来回调我们类中run方法。
这个时候我们需要重写MyThread.java
public class MyThread {
static {
//装载自己的库
System.loadLibrary("MyThreadNative");
}
public static void main(String[] args) {
MyThread myThread = new MyThread();
myThread.start0();
}
//这个run方法,要让C程序调用到,就完美了
public void run(){
System.out.println("I am java Thread !!");
}
private native void start0();
}
现在我们只需要将原来MyThread.c中代码修改,利用JNI反向调用MyThread类中run方法即可模拟出Java的线程了。当我打开MyThread.c的文件的时候,我傻了,怎么在thread_entity获取到JNIEnv对象?
于是开始我的尝试:
-
尝试一:利用全局变量将其传出去,很可惜失败了。下面是错误的代码
#include <pthread.h> #include <stdio.h> #include "MyThread.h"//记得导入刚刚编译的那个.h文件 pthread_t pid; JNIEnv *env; void* thread_entity(void* arg){ jclass cls; jobject obj; jmethodID cid; jmethodID rid; jint ret = 0; //通过虚拟机找到Java的类(MyThread) cls = (*env)->FindClass(env, "MyThread"); if (cls == NULL) { printf("FindClass error!\n"); } //找到MyThread类中无参的构造函数 cid = (*env)->GetMethodID(env, cls, "<init>", "()V"); if (cid == NULL) { printf("Query constructor error!\n"); } //实例化对象 obj = (*env)->NewObject(env, cls, cid); if (obj == NULL) { printf("NewObject error!\n"); } //查找run方法 rid = (*env)->GetMethodID(env, cls, "run", "()V"); if (rid == NULL) { printf("Query runMethod error\n"); } while (1) { usleep(100); //调用run方法 ret = (*env)->CallIntMethod(env, obj, rid, NULL); } } //这个方法要参考.h文件的15行代码,这里的参数得注意,你写死就行,不用明白为什么 JNIEXPORT void JNICALL Java_MyThread_start0(JNIEnv *oldEnv, jobject c1){ env = oldEnv; pthread_create(&pid,NULL,thread_entity,NULL); while(1){ usleep(100); printf("I am main\n"); } } int main(){ return 0; }
-
尝试二:利用搜索引擎查找到了c语言中嵌套Java虚拟机,很可惜还是失败了,下面是错误的代码
#include <pthread.h> #include <stdio.h> #include "MyThread.h"//记得导入刚刚编译的那个.h文件 pthread_t pid; void* thread_entity(void* arg){ int res; JavaVM *jvm; JNIEnv *env; JavaVMInitArgs vm_args; JavaVMOption options[3]; /*设置初始化参数*/ options[0].optionString = "-Djava.compiler=NONE"; options[1].optionString = "-Djava.class.path=."; options[2].optionString = "-verbose:jni"; //用于跟踪运行时的信息 /*版本号设置不能漏*/ vm_args.version = JNI_VERSION_1_8; vm_args.nOptions = 3; vm_args.options = options; vm_args.ignoreUnrecognized = JNI_TRUE; res = JNI_CreateJavaVM(&jvm, (void**)&env, &vm_args); jclass cls; jobject obj; jmethodID cid; jmethodID rid; jint ret = 0; //通过虚拟机找到Java的类(MyThread) cls = (*env)->FindClass(env, "MyThread"); if (cls == NULL) { printf("FindClass error!\n"); } //找到MyThread类中无参的构造函数 cid = (*env)->GetMethodID(env, cls, "<init>", "()V"); if (cid == NULL) { printf("Query constructor error!\n"); } //实例化对象 obj = (*env)->NewObject(env, cls, cid); if (obj == NULL) { printf("NewObject error!\n"); } //查找run方法 rid = (*env)->GetMethodID(env, cls, "run", "()V"); if (rid == NULL) { printf("Query runMethod error\n"); } while (1) { usleep(100); //调用run方法 ret = (*env)->CallIntMethod(env, obj, rid, NULL); } } //这个方法要参考.h文件的15行代码,这里的参数得注意,你写死就行,不用明白为什么 JNIEXPORT void JNICALL Java_MyThread_start0(JNIEnv *env, jobject c1){ pthread_create(&pid,NULL,thread_entity,NULL); while(1){ usleep(100); printf("I am main\n"); } } int main(){ return 0; }
-
尝试三:搜出来说我路径写错了,于是又修改了一遍路径,进行尝试,很可惜还是失败了,下面是错误的代码
#include <pthread.h> #include <stdio.h> #include "MyThread.h"//记得导入刚刚编译的那个.h文件 pthread_t pid; void* thread_entity(void* arg){ int res; JavaVM *jvm; JNIEnv *env; JavaVMInitArgs vm_args; JavaVMOption options[1]; /*设置初始化参数*/ options[0].optionString = "-Djava.class.path=/usr/lib/jvm/java-1.8.0-openjdk-amd64/"; /*版本号设置不能漏*/ vm_args.version = JNI_VERSION_1_8; vm_args.nOptions = 1; vm_args.options = options; vm_args.ignoreUnrecognized = JNI_FALSE; res = JNI_CreateJavaVM(&jvm, (void**)&env, &vm_args); jclass cls; jobject obj; jmethodID cid; jmethodID rid; jint ret = 0; //通过虚拟机找到Java的类(MyThread) cls = (*env)->FindClass(env, "MyThread"); if (cls == NULL) { printf("FindClass error!\n"); } //找到MyThread类中无参的构造函数 cid = (*env)->GetMethodID(env, cls, "<init>", "()V"); if (cid == NULL) { printf("Query constructor error!\n"); } //实例化对象 obj = (*env)->NewObject(env, cls, cid); if (obj == NULL) { printf("NewObject error!\n"); } //查找run方法 rid = (*env)->GetMethodID(env, cls, "run", "()V"); if (rid == NULL) { printf("Query runMethod error\n"); } while (1) { usleep(100); //调用run方法 ret = (*env)->CallIntMethod(env, obj, rid, NULL); } } //这个方法要参考.h文件的15行代码,这里的参数得注意,你写死就行,不用明白为什么 JNIEXPORT void JNICALL Java_MyThread_start0(JNIEnv *env, jobject c1){ pthread_create(&pid,NULL,thread_entity,NULL); while(1){ usleep(100); printf("I am main\n"); } } int main(){ return 0; }
-
尝试四:快睡觉之前,我突然想到了安卓底层硬件要调用Java的函数,只能在c语言中调用,它是怎么做的呢?于是搜了一下,最后仿照示例写了一段代码,终于成功了。代码如下
#include <pthread.h> #include <stdio.h> #include "MyThread.h"//记得导入刚刚编译的那个.h文件 pthread_t pid; JavaVM *javaVM; void *thread_entity(void *arg) { JNIEnv* env = NULL; //从jvm对象中取得JNIEvn对象 (*javaVM)->AttachCurrentThread(javaVM,&env,NULL); jclass cls; jobject obj; jmethodID cid; jmethodID rid; jint ret = 0; //通过虚拟机找到Java的类(MyThread) cls = (*env)->FindClass(env, "MyThread"); if (cls == NULL) { printf("FindClass error!\n"); } //找到MyThread类中无参的构造函数 cid = (*env)->GetMethodID(env, cls, "<init>", "()V"); if (cid == NULL) { printf("Query constructor error!\n"); } //实例化对象 obj = (*env)->NewObject(env, cls, cid); if (obj == NULL) { printf("NewObject error!\n"); } //查找run方法 rid = (*env)->GetMethodID(env, cls, "run", "()V"); if (rid == NULL) { printf("Query runMethod error\n"); } while (1) { usleep(100); //调用run方法 ret = (*env)->CallIntMethod(env, obj, rid, NULL); } } //这个方法要参考.h文件的15行代码,这里的参数得注意,你写死就行,不用明白为什么 JNIEXPORT void JNICALL Java_MyThread_start0(JNIEnv *oldEnv, jobject c1) { pthread_create(&pid, NULL, thread_entity, NULL); while (1) { usleep(100); printf("I am main\n"); } } //jvm启动的时候调用,将jvm对象返回出去 JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved){ javaVM = vm; return JNI_VERSION_1_8; } int main() { return 0; }
然后就解析类,把这个MyThread.c编译成为一个动态链接库,这样在java代码里会被load到内存libMyThreadNative这个命名需要注意的libxx,xx这里等于你java那边写的字符串,具体指令如下:
gcc -fPIC -I /usr/lib/jvm/java‐1.8.0‐openjdk-amd64/include -I /usr/lib/jvm/java-1.8.0-openjdk-amd64/include/linux -shared -o libMyThreadNative.so MyThread.c
上面的warning不用管,接下来将我们这个.so文件加入到path,这样java才能load的到,具体指令如下:
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:{libMyThreadNative.so}所在的路径
最后执行,发现已经成功了,可以回调我们Java类中run方法了,至此我们模拟的Java中Thread类已经完成了。
总结:java中线程和操作系统中线程是一一对应的,当调用线程的start方法,会调用线程中本地方法start0,然后会调用Thread.c中JVM_StartThread,然后会调用jvm.cpp中对应的方法,再然后会调用操作系统中pthread_create函数来创建线程,pthread_create创建好线程回调java类中run方法。
![Logo](https://devpress.csdnimg.cn/79de2bf0b7994defa4242ef90d5513fa.jpg)
开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!
更多推荐
所有评论(0)