这就是程序猿的快乐吧

导航

java线程的本质、线程模型

java线程的本质、线程模型

1.java线程和操作系统(linux)的线程是什么关系?


​ 如上图所示!我们在java代码当中创建线程new Thread(),启动线程需要调用start方法进入就绪状态(不会立马创建线程),继而start方法当中调用了jni(java本地方法)方法start0,在执行start0方法没有出现异常情况,线程启动成功。其中调用本地方法的时候会调用到操作系统(linux,centos,Ubuntu等)层面的本地类库的创建线程的方法,不同操作系统调用的函数也不一样,在jdk源码当中os文件夹下有不同操作系统的文件夹去对应调用系统创建线程的函数,例如linux的话调用glibc库,jdk源码src/os/linux/os_linux.cpp/文件夹调用的就是pthread_create函数传入所需参数去创建线程,其中void (start_routine)(void *)参数为线程启动后的主体函数,线程启动成功后调用主体函数,回调java中的run方法执行我们所需的业务逻辑 。
如图所示,Java->Thread.c->start()->start0调用本地方法start0:

进而找到JVM_StartThread方法调用pthread_create函数创建线程。

输入命令:

man pthread_create

​ 如图所示:上边解释了该函数的定义

根据man配置的信息可以得出pthread_create会创建一个线程,这个函数是linux系统的函数,可以用C 或者C++直接调用,上面信息也告诉程序员这个函数在pthread.h, 这个函数有四个参数:

*参数名字* *参数定义* *参数解释*
pthread_t *thread 传出参数,调用之后会传出被创建线程的id 定义 pthread_t pid; 继而 取地址&pid
const pthread_attr_t*attr 线程属性,关于线程属性是linux 的知识 一般传NULL,保持默认属性
void (start_routine) (void *) 线程的启动后的主体函数 需要你定义一个函数,然后传函数名即可
void *arg 主体函数的参数 没有可以传nulll

接下来

2.linux上启动一个线程的代码。

    //头文件
    #include <pthread.h> 
    #include <stdio.h>
    //定义一个变量,接受创建线程后的线程id
    pthread_t pid;
    //定义线程的主体函数
    void thread_entity(void	arg) {
        printf(" new Thread! from c");
    }
	//main方法,程序入口,main和java的main一样会产生一个进程,继而产生一个main线程
	int main() {
    	//调用操作系统的函数创建线程,注意四个参数
        pthread_create(&pid,NULL,thread_entity,NULL);
        //usleep是睡眠的意思,那么这里的睡眠是让谁睡眠呢?让主线程睡眠。
        //为什么需要睡眠?如果不睡眠会出现什么情况。:因为如果主线程不睡眠,子线程没有抢占到cpu资源没有创建好线程,
        //主线程先抢占到资源执行的话,在线程还没创建好之前,主线程结束,子线程就不会运行。
        //主线程睡眠过程当中,把资源分给子线程,
        //子线程执行。
        usleep(100);
        printf("main\n"); 
        return 0;
	}

假设有了上面知识的铺垫,那么可以试想一下java的线程模型到底是什么情况呢?

3.在java代码里启动一个线程的代码

public class Example4Start {
	public static void main(String[] args) { 
	Thread thread = new Thread(){
			@Override
			public void run() {
				System.out.println("new Thread from java ! ");
				}
		};
	thread.start();
	}
}

这里启动的线程和上面我们通过linux的pthread_create函数启动的线程有什么关系呢?只能去查 看start()的源码了,看看java的start()到底干了什么事才能对比出来。start方法的源码的部分截图

可以看到这个方法最核心的就是调用了一个start0方法,而start0方法又是一个native方法,故而如果要 搞明白start0我们需要查看Hotspot的源码,好吧那我们就来看一下Hotspot的源码吧,Hotspot的源码 怎么看么?一般直接看openjdk的源码,openjdk的源码如何查看、编译调试?openjdk的编译我们后面会讨论,在没有openjdk的情况下,我们做一个大胆的猜测,java级别的线程其实就是操作系统级别的线程,什么意思呢?说白了我们大胆猜想 start----->start0--->ptherad_create

4.根据上边猜想模拟实现java启动线程

    public static void main(String[] args) {
        //自己定义的类
        OwnThread enjoyThread = new OwnThread();
        enjoyThread.starts();
    }

    private native void starts();

这里我们让自己写的starts调用一个本地方法,在本地方法里面去启动一个系统线程,我们写一个c 程序来启动本地线程

5.本地方法的代码编写

编写c语言头文件pthread.c

    #include <pthread.h> 
    #include <stdio.h>
    //定义变量接受线程id
    pthread_t pid;
    //线程的主体方法相当于 java当中的run 
    void* thread_entity(void* arg) {
        //子线程死循环
        while(1){
            //睡眠100毫秒
            usleep(100);
            //打印
            printf("Thread\n");
        }
    }
    //c语言的主方法入口方法,相当于java的main 
    int main() {
        //调用linux的系统的函数创建一个线程
        pthread_create(&pid,NULL,thread_entity,NULL);
        //主线程死循环
        while(1){
            //睡眠100毫秒
            usleep(100);
            //打印
            printf("main\n");
        }
            return 0;
    }

6.在linux上编译运行上述c程序

编译这个程序(-o 标识指定编译后的文件名,也可以不指定,默认为a.out)

gcc pthread.c -o pthread.out -pthread

运行这个程序

./pthread.out

结果如下所示:

Thread
main
Thread
main
Thread
main
Thread
main
Thread
main
Thread
main

结果是两个线程一直在交替执行,得到我们预期的结果。现在的问题就是我们如何通过starts调用这个c 程序,这里就要用到JNI了

7.自定义JNI本地native方法

​ 这里我们用代码来演示整个的操作过程:

  1. 之前我们编写好的java类。

    package com.thread.study;
    
    public class OwnThread {
    
        //装载库,保证JVM在启动的时候就会装载,故而一般是也给static
        static {
            System.loadLibrary("OwnThreadNative");
        }
    
        public static void main(String[] args) {
            OwnThread enjoyThread = new OwnThread();
            enjoyThread.starts();
        }
    
        private native void starts();
    }
    
    
  2. 上传到linux系统上边,用javac命令编译好这个java类(这里的编译是个提前操作,主要用来后边的测试),在java文件包目录下运行编译命令javac

    javac OwnThread.java
    

    编译好之后在目录下除了OwnThread.java文件之外还会多一个OwnThread.class的文件。

  3. 使用java命令将我们编写的java类编译成以.h结尾的头文件(c语言),这个命令要根据jdk版本去使用,java8的话命令是java -h xxx.java,java11中是javac -h . xx.java,编译的目录最好是和java文件在同一个目录(java文件包路径下),此处需要注意的是java11的命令-h后一定要有. 没有的话会报错无源文件

    javac -h . OwnThread.java
    

    编译好的结果是在同目录下生成一个名叫com_thread_study_OwnThread.h的文件。

  4. 接下来把上边写好的pthread.c头文件方法名修改为我们可调用的方法名,复制pthread.c为pthreadBack.c,接下来的修改操作在备份文件pthreadBack.c来进行,打开第3步编译好的文件com_thread_study_OwnThread.h

    cat com_thread_study_OwnThread.h
    

    可以看到里边生成文件。

    JNIEXPORT void JNICALL Java_com_thread_study_OwnThread_starts(JNIEnv *env, jobject){
    
    }
    

    方法名:Java_com_thread_study_OwnThread_starts复制到备份文件pthreadBack.c中 记得导入之前编译好的com_thread_study_OwnThread.h文件

    #include <pthread.h> 
    #include <stdio.h>
    #include "com_thread_study_OwnThread.h"//记得导入之前编译好的com_thread_study_OwnThread.h文件
    //定义变量接受线程id
    pthread_t pid;
    //线程的主体方法相当于 java当中的run 
    void* thread_entity(void* arg) {
    	//子线程死循环
    	while(1){
    		//睡眠100毫秒
    		usleep(100);
    		//打印
    		printf("Thread\n");
    	}
    }
    //这个方法名字需要复制上边com_thread_study_OwnThread.h当中的方法名字,打开.h文件,复制方法名过来 参数是固定的
    Java_com_thread_study_OwnThread_starts(JNIEnv *env, jobject c1) {
            //调用linux的系统的函数创建一个线程
            pthread_create(&pid,NULL,thread_entity,NULL);
            //主线程死循环
            while(1){
                //睡眠100毫秒
                usleep(100);
                //打印
            	printf("main\n");
    		}
    	}
    
  5. 编译pthreadBack.c为so文件,成为so文件之后才能被java加载,编译命令:

    gcc	-fPIC -I /usr/jdk/jdk11/include -I /usr/jdk/jdk11/include/linux -shared -o libOwnThreadNative.so pthreadBack.c
    

    此处libOwnThreadNative.so ,lib是固定的,OwnThreadNative需要和Java代码中System.loadLibrary("OwnThreadNative")的值一致

        static {
        	//如果你是libabc;这里就写abc
            System.loadLibrary("OwnThreadNative");
        }
    

    我编译好的目录如下:

    com_thread_study_OwnThread.h
    OwnThread.class
    OwnThread.java
    libOwnThreadNative.so
    pthreadBack.c
    
  6. 接下来把生成的so文件所在的目录添加到系统变量,否则java文件load不到生成的so文件

    export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/usr/thread/com/thread/study/
    

    命令:后边的文件路径是我们生成的so文件的路径

  7. 运行上边写好的java文件,运行java文件的时候切出java包目录

    java  com.thread.study.OwnThread
    

    大概的效果如下:

    Thread
    main
    Thread
    Thread
    main
    Thread
    main
    Thread
    main
    main
    Thread
    main
    Thread
    Thread
    main
    

    可以看到两个线程在跑,其中一个线程是使用java调用自定义的本地方法启动的

注意:以上代码运行的服务器环境是linux的,至于这里为何不用windows的 ,因为windows不开源,不知道windows里边创建线程需要调用创建线程的函数是什么。

posted on 2022-02-23 20:34  这就是程序猿的快乐吧  阅读(142)  评论(0编辑  收藏  举报