并发系列一:初始java线程与os的关系,模拟java调用os函数创建线程
程序员文章站
2022-05-03 20:49:54
...
前言
- 并发,这是一个值得深思的话题。它似无形却有形。我们平常的工作都是面向业务编程,CRUD居多,基本上与并发没什么交集。ok,并发是一个广泛的概念。那么咱们来聊聊多线程(java 多线程)。这里咱们来思考下问题:为什么要使用多线程?俗话说,一方有难八方支援。在今年的疫情初期,武汉的疫情非常严峻,如果仅靠武汉的白衣天使来医治病患,这无疑是一个长征项目,这就等同于单线程在干活。于是一批批来自于五湖四海的白衣天使前往武汉进行支援(点赞!),此时就是多线程在协同工作。是的,你没想错,使用多线程的就是为了加快程序运行速度。换句话来说,就是提高cpu利用率。如果把国家的每个行政区比作cpu的一个核,那么咱们国家就是一个34核的cpu。试问下,一个核和34个核的处理速度,这不用我说,大家都懂吧!
- 上述的描述,可以得出一个结论:java线程与操作系统是等价的。接下来,咱们来证明下此结论
一、证明java线程等价于os的线程
- 为了能正常的看出效果,我选择window系统的任务管理器来证明这个结论
- 编写如下代码(InitThread.java):
public class InitThread { public static void main(String[] args) { for (int i = 0; i < 100; i++) { new Thread(() -> { try { Thread.sleep(100000); } catch (InterruptedException e) { e.printStackTrace(); } }).start(); } } }
- 执行代码之前,打开任务管理器,并查看线程数,如下图所示:
- 运行上述java类,然后再次查看任务管理器
-
综上,可以看出,我们使用java创建出一个线程与os中创建出一个线程是对等的。因此可以判断,
java创建线程时与os函数进行了交互
。于是,我们来看下Thread类的start方法 - java.lang.Thread#start
// 上面部分代码省略 boolean started = false; try { // 调用了此方法开启了线程 start0(); started = true; } finally { // finally部分代码省略 }
- java.lang.Thread#start0
// start0方法时一个native方法 private native void start0();
- 可以看到java调用Thread的start方法启动一个线程时,最终会调用到start0方法。而start0是native方法,何为native方法?什么是native方法呢?为了解释native方法,这里要描述下java的发展历史,很久很久以前,c语言很流行,所有的程序基本上都是c写的。在1995年,Java诞生了,它以不需要手动释放内存的特性深受程序员欢迎,java的开发团队为了解决java与c的通讯问题,所以使用c/c++写出了jvm。jvm在java中起到了非常大的作用,包括垃圾回收器、java与os的交互、与c语言的交互等等。native方法就是对应的一个c语言文件后java在调用它时是通过jvm来交互的
二、使用自定义native方法开启一个线程
2.1 使用os函数开启一个线程
-
ps:此时我选择的os为centos7 64位的os(拥有c语言编译环境)
-
第一步:查看os创建线程的api
#1. 安装man命令 => 为了查看函数信息 yum install man-pages #2. 执行如下命令查看os创建线程api,具体内容查看下图 man pthread_create
-
第二步:使用os的api(pthread_create)创建一个线程
1.撰写myThread.c文件
#include "pthread.h" //头文件,在pthread_create方法中有明确写到 #include "stdio.h" pthread_t pid; // 定义一个变量,用来存储生成的线程id, 在pthread_create方法中也有介绍 /** * 定义主体函数 */ void* run(void* arg) { while(1) { printf("\n Execting run function \n"); printf(arg); sleep(1); } } /** * 若要编译成可执行文件,则需要写main方法 */ int main() { pthread_create(&pid, NULL, run, "123"); // 调用os创建线程api while(1) { // 这里必须要写个死循环,因为c程序在main方法执行结束后,它内部开的子线程也会关掉 } }
2.编译c文件成可执行命令
# -pthread参数表示把pthread类库也添加到编译范围 gcc -o myThread myThread.c -pthread
-
第三步:运行并查看结果
运行编译后的c文件
./myThread
运行结果:
综上,咱们已经使用os函数启动了一个线程
2.2 使用java调用自定义的native方法启动线程
- 第一步:创建
ExecMyNativeMethod.java
类(不用指定在哪个包下,因为最终要把它放在linux中去执行)public class ExecMyNativeMethod { /** * 加载本地方法类库,注意这个名字,后面会用到 */ static { System.loadLibrary("MyNative"); } public static void main(String[] args) { ExecMyNativeMethod execMyNativeMethod = new ExecMyNativeMethod(); execMyNativeMethod.start0(); } private native void start0(); }
- 第二步:将java类编译成class文件
javac ExecMyNativeMethod.java
- 第三步:将class文件转成c语言头文件
javah ExecMyNativeMethod
- 第四步:查看编译后的头文件
对于上述内容,我们只需要关注我们定义的native方法(/* DO NOT EDIT THIS FILE - it is machine generated */ #include <jni.h> /* Header for class ExecMyNativeMethod */ #ifndef _Included_ExecMyNativeMethod #define _Included_ExecMyNativeMethod #ifdef __cplusplus extern "C" { #endif /* * Class: ExecMyNativeMethod * Method: start0 * Signature: ()V */ JNIEXPORT void JNICALL Java_ExecMyNativeMethod_start0 (JNIEnv *, jobject); #ifdef __cplusplus } #endif #endif
JNIEXPORT void JNICALL Java_ExecMyNativeMethod_start0 (JNIEnv *, jobject);
)即可,也就是说native方法转成c语言头文件后会变成JNIEXPORT void JNICALL Java_类名_native方法名 (JNIEnv *, jobject);
的格式 - 第五步:更新我们刚刚编写的
myThread.c
文件,为了不造成影响,我们使用cp命令创建出一个新的c文件myThreadNew.ccp myThread.c myThreadNew.c
- 第六步:修改myThreadNew.c文件为如下内容
#include "pthread.h" // 引用线程的头文件,在pthread_create方法中有明确写到 #include "stdio.h" #include "ExecMyNativeMethod.h" // 将自定义的头文件导入 pthread_t pid; // 定义一个变量,用来存储生成的线程id, 在pthread_create方法中也有介绍 /** * 定义主体函数 */ void* run(void* arg) { while(1) { printf("\n Execting run function \n"); printf(arg); sleep(1); } } /** * 此方法就是后面java要调用到的native方法 */ JNIEXPORT void JNICALL Java_ExecMyNativeMethod_start0(JNIEnv *env, jobject c1) { pthread_create(&pid, NULL, run, "Creating thread from java application"); // 调用os创建线程api while(1) {} // 死循环等待 } /** * 每个要执行的c文件都要写main方法, * 如果要编译成动态链接库,则不需要 */ int main() { return 0; }
- 第七步:执行如下命令将
myThreadNew.c
文件编译成动态链接库,并添加到环境变量中(否则在启动java类的main方法时,在静态代码块中找不到myNative
类库)# 1. 编译成动态链接库 # 说明下-I后面的参数: 分别指定jdk安装目录的include文件夹和include/linux文件夹 # 因为我在环境变量中配置了JAVA_HOME,所以我直接$JAVA_HOME了 # 后面的libMyNative.so文件,它的格式为lib{xxx}.so # 其中{xxx}为类中System.loadLibrary("yyyy")代码中yyyy的值 gcc -I $JAVA_HOME/include -I $JAVA_HOME/include/linux -fPIC -shared -o libMyNative.so myThreadNew.c # 2. 将此动态链接库添加到环境变量中 # 格式: export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:{libxxxx.so} # 其中{libxxxxNative.so}为动态链接库的路径, # 我的libMyNative.so文件在/root/workspace文件夹下 export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/root/workspace/libMyNative.so
- 第八步:执行如下命令启动java程序
java ExecMyNativeMethod
- 查看运行结果
综上,我们仅仅是使用java调用了自己编写的native方法启动了线程。如果我们要和java一样,自己写一个run方法,然后启动线程时,来调用这个run方法的话,要怎么实现呢?别急,往下看!
2.3 native方法回调java方法
- 第一步:优化我们的
ExecMyNativeMethod.java
类,新增run方法,具体如下:public class ExecMyNativeMethod { /** * 加载本地方法类库,注意这个名字,后面会用到 */ static { System.loadLibrary("MyNative"); } public static void main(String[] args) { ExecMyNativeMethod execMyNativeMethod = new ExecMyNativeMethod(); execMyNativeMethod.start0(); } private native void start0(); public void run() { System.out.println("I'm run method.........."); } }
- 第二步:修改上述的
myThreadNew.c
文件为如下内容(用到了JNI
,这个c文件在jdk的安装目录中可以找到,所以这是jdk提供的功能):#include "stdio.h" #include "ExecMyNativeMethod.h" // 将自定义的头文件导入 #include "jni.h" /** * 此方法就是后面java要调用到的native方法 */ JNIEXPORT void JNICALL Java_ExecMyNativeMethod_start0(JNIEnv *env, jobject c1) { jclass cls = (*env)->FindClass(env, "ExecMyNativeMethod"); if (cls == NULL) { printf("Not found class!"); return; } jmethodID cid = (*env)->GetMethodID(env, cls, "<init>", "()V"); if (cid == NULL) { printf("Not found constructor!"); return; } jobject obj = (*env)->NewObject(env, cls, cid); if (obj == NULL) { printf("Init object failed!"); return; } jmethodID rid = (*env)->GetMethodID(env, cls, "run", "()V"); jint ret = (*env)->CallIntMethod(env, obj, rid, NULL); printf("Finished!"); }
- 第三步:将
myThreadNew.c
文件编译成动态链接库
gcc -I $JAVA_HOME/include -I $JAVA_HOME/include/linux -fPIC -shared -o libMyNative.so myThreadNew.c
- 第四步:编译java类并执行它
javac ExecMyNativeMethod.java java ExecMyNativeMethod
- 查看运行结果:
2.4 额外总结
- 关于用户态和内核态。咱们把它理解成两个角色。用户态理解成普通用户。内核态理解成超级管理员。当普通用户要使用超级管理员的权限时,需要有一个普通用户转化为超级管理员的过程。即所说的用户态转内核态。大家可以想象下,在ubuntu系统下,我们的一个普通用户要使用管理员的权限是不是要在命令前面添加
sudo
命令?这也是一个转化。
三、总结
- 综上,咱们了解了java线程与os的关系,以及模拟了java调用os函数创建线程的流程。
- 最近在学习并发相关的知识点, 后续将会继续更新。下篇文章主题为:
synchronized关键字常见api、初始对象头以及证明hashcode
。 - 并发模块对应github地址:传送门
- I am a slow walker, but I never walk backwards.