前言

  • 并发,这是一个值得沉思的话题。它似有形却无形。咱们平时的工作都是面向业务编程,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 __cplusplusextern "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.c

    cp 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.javajava ExecMyNativeMethod
  • 查看运行后果:

    2.4 额定总结

  • 对于用户态和内核态。咱们把它了解成两个角色。用户态了解成普通用户。内核态了解成超级管理员。当普通用户要应用超级管理员的权限时,须要有一个普通用户转化为超级管理员的过程。即所说的用户态转内核态。大家能够设想下,在ubuntu零碎下,咱们的一个普通用户要应用管理员的权限是不是要在命令后面增加sudo命令?这也是一个转化。

三、总结

  • 综上,咱们理解了java线程与os的关系,以及模仿了java调用os函数创立线程的流程
  • 最近在学习并发相干的知识点, 后续将会持续更新。下篇文章主题为:synchronized关键字常见api、初始对象头以及证实hashcode
  • 并发模块对应github地址:传送门
  • I am a slow walker, but I never walk backwards.