Java线程与操作系统线程的关系

Itachi 2020年05月30日 155次浏览

[TOC]

万字长文,阅读约耗费15分钟...

操作系统的线程

Linux操作系统启动一个线程

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

再Linux系统中输入命令 man pthread_create后如下图所示。

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

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
  1. 在Linux上启动一个线程的代码,定义一个后缀名为 .c 的文件
// 头文件
#include <pthread.h>
#include <stdio.h>
// 定义一个变量,接受创建线程后的线程id
pthread_t pid;
// 定义线程的主体函数
void* thread_entity(void* arg)
{
    printf("I am new thread!\n");
}
// main方法,程序入口,main和java的main一样会产生一个进程,继而产生一个main线程
int main()
{
        while(1){
                // 调用操作系统的函数创建线程,注意四个参数
                pthread_create(&pid,NULL,thread_entity,NULL);
                // usleep是睡眠的意思
                usleep(100);
                printf("I am main thread...\n");
        }
}
  1. 执行命令编译 c 文件gcc -o thread.out thread.c -pthread

thread.outthread.c 编译成功之后的文件

  1. 运行:./thread.out
  2. 输出:

I am new thread!
I am main thread...
I am new thread!
I am main thread...
I am new thread!
I am main thread...
I am new thread!
...
一直交替执行...

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

Java里的线程

在Java代码里启动一个线程

/**
 * @author Itachi is_xianglei@163.com
 * @Date 2020-05-30 12:38
 */
public class Demo {

    public static void main(String[] args) {
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("I am thread for java");
            }
        });
        t1.start();
    }
}

猜想Java中的线程就是调用操作系统的线程

这里启动的线程和上面通过Linuxpthread_create函数启动的线程有什么关系呢?

只能去查看start()的源码了,看看java的start()到底干了什么事才能对比出来.

image.png

可以看到这个方法最核心的就是调用了一个start0()方法,而start0()方法又是一个native方法,故而如果要搞明白start0我们需要查看Hotspot的源码, 那我们就来看一下Hotspot的源码, 一般直接看openjdk的源码,我们先做一个大胆的猜测,java级别的线程其实就是操作系统级别的线程 也就是说:

我们调用start() ---> start0() ---> ptherad_create() 鉴于这个猜想来模拟实现一下!

验证Java中的线程就是调用操作系统的线程

/**
 * @author Itachi is_xianglei@163.com
 * @Date 2020-05-30 12:38
 */
public class Demo {

    private native void start0();
  
    static {
        System.loadLibrary("TestThreadNative");
    }
    
    public static void main(String[] args) {
        Demo demo = new Demo();
        demo.start0();
    }
}

这里我们自己写的start0调用一个本地方法,在本地方法里面去启动一个系统线程.

System.loadLibrary()装载库,保证JVM在启动的时候就会装载。

然后我们写一个c程序来启动本地线程,文章开始时,写好了一段C程序,就用哪个吧。

// 头文件
#include <pthread.h>
#include <stdio.h>
// 定义一个变量,接受创建线程后的线程id
pthread_t pid;
// 定义线程的主体函数
void* thread_entity(void* arg)
{
    printf("I am new thread!\n");
}
// main方法,程序入口,main和java的main一样会产生一个进程,继而产生一个main线程
int main()
{
        while(1){
                // 调用操作系统的函数创建线程,注意四个参数
                pthread_create(&pid,NULL,thread_entity,NULL);
                // usleep是睡眠的意思
                usleep(100);
                printf("I am main thread...\n");
        }
}

在Linux上编译并运行上述C程序

gcc thread.c -o thread.out -pthread
./thread.out 

现在Java代码写好了,C程序也写好了。目前的问题就是我们如何通过start0调用这个c程序?

这里就要用到JNI了...(想起来了以前用Java调用易语言的DLL时候了..hhhh)

我们将写好的Java类上传至Linux系统中并编译!

在Linux下编译成clas文件:
编译: javac Demo.java 
生成class文件:Demo.class
在生成 .h 头文件:
编译: javah Demo
生成h文件:Demo.h

查看一下生成的.h文件
image.png

在第15行代码 Java_Demo_start0方法就是我们需要在C程序中定义的方法

然后继续修改.c程序,修改的时候参考.h文件,复制一份.c文件,取名threadNew.c 定义一个方法Java_Demo_start0在方法中启动一个子线程,代码如下:

// 头文件
#include <pthread.h>
#include <stdio.h>
// 记得导入刚刚编译的那个.h文件
#include "Demo.h"
// 定义一个变量,接受创建线程后的线程id
pthread_t pid;
// 定义线程的主体函数
void* thread_entity(void* arg)
{
    printf("I am new thread!\n");
}
// 这个方法要参考.h文件的15行代码
JNIEXPORT void JNICALL Java_Demo_start0(JNIEnv *env, jobject c1){
        pthread_create(&pid,NULL,thread_entity,NULL);
        while(1){
                usleep(100);
                printf("I am main thread\n");
        }
}
// main方法,程序入口,main和java的main一样会产生一个进程,继而产生一个main线程
int main()
{
        return 0;
}

把这个threadNew.c编译成为一个动态链接库,这样在java代码里会被laod到内存libTestThreadNative这个命名需要注意libxx,xx就等于我们java那边写的字符串(System.loadLibrary("TestThreadNative");)

解析类:

gcc ‐fPIC ‐I ${JAVA_HOME}/include ‐I ${JAVA_HOME}/include/linux ‐shared ‐o libTestThreadNative.so threadNew.c

如果执行上面的命令出现如下图的错误后,可尝试下面的这条命令...踩坑好久..原因未知...

image.png

gcc ./threadNew.c -I  /opt/jdk1.8.0_251/include -I /opt/jdk1.8.0_251/include/linux -fPIC -shared -o libTestThreadNative.so

做完这一系列事情之后需要把这个.so文件加入到path,这样java才能load到

export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:{libTestThreadNative.so}

image.png

直接测试,运行我们自己写的那个java类直接测试看看结果能不能启动线程

执行命令 java Demo
image.png

牛逼!我们已经通过自己写的一个类,启动了一个线程。

但是这个线程函数体是不是java的,是C程序的。

接下来我们来实现一下这个run!

模拟实现start方法是如何调用run方法的

...

.....

..........

未完待续..(踩上面那个坑踩了太久了,弄了大半天,心好累,不想写了。以后再说吧)