基于JNI手动模拟Java线程

并发一直是个难点,也是面试的重灾区,闲来无事,就想知道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方法。

在这里插入图片描述

  • 6
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值