并发系列一:初识java线程与os的关系,模拟java调用os函数创建线程

前言

  • 并发,这是一个值得深思的话题。它似无形却有形。我们平常的工作都是面向业务编程,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文件
    ```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
    运行结果:

    在这里插入图片描述

    综上,咱们已经使用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.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.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.
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,332评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,508评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,812评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,607评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,728评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,919评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,071评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,802评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,256评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,576评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,712评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,389评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,032评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,798评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,026评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,473评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,606评论 2 350

推荐阅读更多精彩内容