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方法
这里我们用代码来演示整个的操作过程:
-
之前我们编写好的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(); }
-
上传到linux系统上边,用javac命令编译好这个java类(这里的编译是个提前操作,主要用来后边的测试),在java文件包目录下运行编译命令javac
javac OwnThread.java
编译好之后在目录下除了OwnThread.java文件之外还会多一个OwnThread.class的文件。
-
使用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的文件。
-
接下来把上边写好的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"); } }
-
编译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
-
接下来把生成的so文件所在的目录添加到系统变量,否则java文件load不到生成的so文件
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/usr/thread/com/thread/study/
命令:后边的文件路径是我们生成的so文件的路径
-
运行上边写好的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) 编辑 收藏 举报