前言
- 并发 ,这是一个值得沉思的话题。它似有形却无形。咱们平时的工作都是面向业务编程,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
![在这里插入图片形容](https://img-blog.csdnimg.cn/20200527104128968.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L2F2ZW5nZXJFdWc=,size_16,color_FFFFFF,t_70)
-
第二步:应用 os 的 api(pthread_create)创立一个线程
> 1. 撰写 myThread.c 文件 ```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 文件成可执行命令 ```shell # -pthread 参数示意把 pthread 类库也增加到编译范畴 gcc -o myThread myThread.c -pthread ```
-
第三步:运行并查看后果
> 运行编译后的 c 文件 ```shell ./myThread ``` 运行后果:![在这里插入图片形容](https://img-blog.csdnimg.cn/20200527104724639.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L2F2ZW5nZXJFdWc=,size_16,color_FFFFFF,t_70) 综上,咱们曾经应用 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
-
第四步:查看编译后的头文件
/* 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
对于上述内容,咱们只须要关注咱们定义的 native 办法(`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.