java线程的本质、线程模型

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

在java中绿色线程和本地线程区别 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:

在java中绿色线程和本地线程区别 java 本地线程_创建线程_02


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

输入命令:

man pthread_create

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

在java中绿色线程和本地线程区别 java 本地线程_创建线程_03

根据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方法的源码的部分截图

在java中绿色线程和本地线程区别 java 本地线程_创建线程_04

可以看到这个方法最核心的就是调用了一个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();
}
  1. 上传到linux系统上边,用javac命令编译好这个java类(这里的编译是个提前操作,主要用来后边的测试),在java文件包目录下运行编译命令javac
javac OwnThread.java

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

  1. 使用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的文件。

  1. 接下来把上边写好的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");
		}
	}
  1. 编译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
  1. 接下来把生成的so文件所在的目录添加到系统变量,否则java文件load不到生成的so文件
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/usr/thread/com/thread/study/

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

  1. 运行上边写好的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里边创建线程需要调用创建线程的函数是什么。