1. 程式人生 > >java併發筆記之java執行緒模型

java併發筆記之java執行緒模型

警告⚠️:本文耗時很長,先做好心理準備
java當中的執行緒和作業系統的執行緒是什麼關係?
猜想: java thread —-對應-—> OS thread
Linux關於作業系統的執行緒控制原始碼:pthread_create()
Linux命令:man pthread_create

int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg);

根據man配置的資訊可以得出pthread_create會建立一個執行緒,這個函式是linux系統的函式,可以用C或者C++直接呼叫,上面資訊也告訴程式設計師這個函式在pthread.h, 這個函式有四個引數:

然後我們來在linux上啟動一個執行緒的程式碼:

建立一個字尾名.c的檔案:

//引入標頭檔案
#include <pthread.h>
#include <stdio.h>
//定義一個變數,接受建立執行緒後的執行緒id
pthread_t pid;
//定義子執行緒的主體函式
void* thread_entity(void* arg)
{
    while (1)
    {
        usleep(100);
        printf("i am new Thread!\n");
    }
}
//main方法,程式入口,main和java的main一樣會產生一個程序,繼而產生一個main執行緒
int main()
{
    //呼叫作業系統的函式建立執行緒,注意四個引數
    pthread_create(&pid,NULL,thread_entity,NULL);
    //usleep是睡眠的意思,那麼這裡的睡眠是讓誰睡眠呢?為什麼需要睡眠?如果不睡眠會出現什麼情況
    //讓主執行緒睡眠,目的是為了讓子執行緒執行
    while (1)
    {
        usleep(100);
        printf("main\n");
    }
}                       

執行命令:

gcc -o thread.out thread.c -pthread
Thread.out 是thread.c 編譯成功之後的檔案
執行:./thread.out 
輸出:
i am new Thread!
main
i am new Thread!
main
i am new Thread!
main
i am new Thread!
main
。。。。。。
//一直交替執行

經過以上分析Linux執行緒建立的過程
可以試想一下java 的執行緒模型到底是什麼情況?
分析: java程式碼裡啟動一個執行緒的程式碼:

import java.lang.Thread;
public class ThreadTest {
public static void main(String[] args) {
    Thread thread = new Thread(){
        @Override
        public void run() {
            System.out.println("i am new Thread!\n”)
        }
    };
    thread.start();
    }
}    

這裡啟動的執行緒(start() 方法)和上面我們通過linux的pthread_create()函式啟動的執行緒有什麼關係呢?
只能去可以檢視start()的原始碼了,看看java的start()到底幹了什麼事才能對比出來。

start原始碼
/**
     * Causes this thread to begin execution; the Java Virtual Machine
     * calls the <code>run</code> method of this thread.
     * <p>
     * The result is that two threads are running concurrently: the
     * current thread (which returns from the call to the
     * <code>start</code> method) and the other thread (which executes its
     * <code>run</code> method).
     * <p>
     * It is never legal to start a thread more than once.
     * In particular, a thread may not be restarted once it has completed
     * execution.
     *
     * @exception  IllegalThreadStateException  if the thread was already
     *               started.
     * @see        #run()
     * @see        #stop()
     */
    public synchronized void start() {
        /**
         * This method is not invoked for the main method thread or "system"
         * group threads created/set up by the VM. Any new functionality added
         * to this method in the future may have to also be added to the VM.
         *
         * A zero status value corresponds to state "NEW".
         */
        if (threadStatus != 0)
            throw new IllegalThreadStateException();

        /* Notify the group that this thread is about to be started
         * so that it can be added to the group's list of threads
         * and the group's unstarted count can be decremented. */
        group.add(this);

        boolean started = false;
        try {
            start0();
            started = true;
        } finally {
            try {
                if (!started) {
                    group.threadStartFailed(this);
                }
            } catch (Throwable ignore) {
                /* do nothing. If start0 threw a Throwable then
                  it will be passed up the call stack */
            }
        }
    }

    //start0方法是一個native方法
    //native方法:就是一個java呼叫非java程式碼的介面,該介面方法的實現由非java語言實現,比如C語言。
    private native void start0();

            

根據Start()原始碼可以看到這個方法最核心的就是呼叫了一個start0方法,而start0方法又是一個native方法,故而如果要搞明白start0我們需要檢視Hotspot的原始碼。
好吧那我們就來看一下Hotspot的原始碼吧,Hotspot的原始碼怎麼看麼??一般直接看openjdk的原始碼,openjdk的原始碼如何檢視、編譯除錯?
Mac 10.14.4 編譯openjdk1.9原始碼 及整合clion動態除錯 : https://app.yinxiang.com/fx/b20706bb-ae55-4ec5-a17b-79930e7e67ea
我們做一個大膽的猜測,java級別的執行緒其實就是作業系統級別的執行緒,什麼意思呢?
說白了我們大膽猜想 start()—>start0()—>ptherad_create()
我們鑑於這個猜想來模擬實現一下:
一:自己寫一個start0()方法來呼叫一個 native 方法,在native方法中啟動一個系統執行緒
//java 程式碼

public class TestThread {
public static void main(String[] args) {
    TestThread testThread = new TestThread();
    testThread.start0();
}
    //native方法
    private native void start0();
}

二:然後我們來寫一個c程式來啟動本地執行緒:

#include <pthread.h>
#include <stdio.h>
//定義一個變數,接受建立執行緒後的執行緒id
pthread_t pid;
//定義子執行緒的主體函式
void* thread_entity(void* arg)
{
  while (1)
  {
    usleep(100);
    printf("i am new Thread!\n");
  }
}
//main方法,程式入口,main和java的main一樣會產生一個程序,繼而產生一個main執行緒
int main()
{
  //呼叫作業系統的函式建立執行緒,注意四個引數
  pthread_create(&pid,NULL,thread_entity,NULL);
  //usleep是睡眠的意思,那麼這裡的睡眠是讓誰睡眠呢?為什麼需要睡眠?如果不睡眠會出現什麼情況
  //讓主執行緒睡眠,目的是為了讓子執行緒執行
  while (1)
  {
    usleep(100);
    printf("main\n");
  }
}

三:在Linux上編譯執行C程式:

編譯: gcc -o thread.out thread.c -pthread
執行: ./thread.out 
就會出現執行緒交替執行:
main
i am new Thread!
main
i am new Thread!
main
i am new Thread!
main
。。。。。。

現在的問題就是我們如何通過start0()呼叫這個c程式,這裡就要用到JNI了(JNI自行掃盲)
Java程式碼如下:

public class TestThread {
    static {
        //裝載庫,保證JVM在啟動的時候就會裝載,故而一般是也給static
        System.loadLibrary("TestThread");
    }

    public static void main(String[] args) {
        TestThread testThread = new TestThread();
        testThread.start0();
  }

    private native void start0();
}            
在Linux下編譯成clas檔案:
編譯: javac java1.java 
生成class檔案:java1.class
在生成 .h 標頭檔案:
編譯: javah TestThread
生成class檔案:TestThread.h
.h檔案分析
#include <jni.h>
/* Header for class TestThread */
#ifndef _Included_TestThread
#define _Included_TestThread
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: TestThread
* Method: start0
* Signature: ()V
*/
//15行程式碼, Java_com_luban_concurrency_LubanThread_start0方法就是你需要在C程式中定義的方法
JNIEXPORT void JNICALL Java_TestThread_start0(JNIEnv *, jobject);
    #ifdef __cplusplus
}
#endif
#endif

然後繼續修改.c程式,修改的時候參考.h檔案,複製一份.c檔案,取名threadNew.c 定義一個Java_com_luban_concurrency_LubanThread_start0 方法在方法中啟動一個子執行緒:

#include <pthread.h>
#include <stdio.h>
//記得匯入剛剛編譯的那個.h檔案
#include "TestThread.h"
//定義一個變數,接受建立執行緒後的執行緒id
pthread_t pid;
//定義子執行緒的主體函式
void* thread_entity(void* arg)
{
    while (1)
    {
        usleep(100);
    printf("i am new Thread!\n");
    }
}    
//這個方法要參考.h檔案的15行程式碼
JNIEXPOR void JNICALL Java_com_luban_concurrency_LubanThread_start0(JNIEnv *env, jobject c1){
  pthread_create(&pid,NULL,thread_entity,NULL);
  while(1)
  {
    usleep(100);
    printf("I am Java_com_luban_concurrency_LubanThread_start0 \n");
  }
}

解析類,把這個threadNew.c編譯成為一個動態連結庫,這樣在java程式碼裡會被laod到記憶體libTestThreadNative這個命名需要注意libxx,xx就等於你java那邊寫的字串

gcc ‐fPIC ‐I ${JAVA_HOME}/include ‐I ${JAVA_HOME}/include/linux ‐shared ‐o libTestThreadNative.so threadNew.c
//需要把這個.so檔案加入到path,這樣java才能load到:
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:{libLubanThreadNative.so}

直接測試,執行我們自己寫的那個java類直接測試看看結果能不能啟動執行緒:
執行:java java1
現象:
main
I am Java_com_luban_concurrency_LubanThread_start0
main
I am Java_com_luban_concurrency_LubanThread_start0
main
I am Java_com_luban_concurrency_LubanThread_start0
main
。。。。。。
我們已經通過自己寫的一個類,啟動了一個執行緒,但是這個執行緒函式體是不是java的是C程式的,這個java執行緒的run方法不同。接下來我們來實現一下這個run:(C來呼叫java的方法,是jni反呼叫java方法)
java的程式碼裡面提供一個run方法:

public class TestThread {
static {
//裝載庫,保證JVM在啟動的時候就會裝載,故而一般是也給static
System.loadLibrary("TestThread");
}

public static void main(String[] args) {
  TestThread testThread = new TestThread();
  testThread.start0();
}

//這個run方法,要讓C程式設計師呼叫到,就完美了
public void run(){
  System.out.println("I am java Thread !!");
}

private native void start0();
}

C程式:

#include <pthread.h>
#include <stdio.h>
//記得匯入剛剛編譯的那個.h檔案
#include "TestThread.h"
//定義一個變數,接受建立執行緒後的執行緒id
pthread_t pid;
//定義子執行緒的主體函式
void* thread_entity(void* arg)
{
  run();
}
//JNIEnv *env 相當於虛擬機器
JNIEXPOR void JNICALL Java_com_luban_concurrency_LubanThread_start0(JNIEnv *env, jobject c1){
  //定一個class 物件
  jclass cls;
  jmethodID cid;
  jmethodID rid;
  //定一個物件
  jobject obj;
  jint ret = 0;
  //通過虛擬機器物件找到TestThread java class
  cls = (*env)->FindClass(env,"TestThread");
  if(cls == NULL){
    printf("FindClass Error!\n")
    return;
  }
  cid = (*env)->GetMethodID(env, cls, "<init>", "()V");
  if (cid == NULL) {
    printf("GetMethodID Error!\n");
    return;
  }
  //例項化一個物件
  obj = (*env)->NewObject(env, cls, cid);
  if(obj == NULL){
    printf("NewObject Error!\n")
    return;
  }
  rid = (*env)->GetMethodID(env, cls, "run", "()V");
  ret = (*env)->CallIntMethod(env, obj, rid, Null);
  printf("Finsh call method!\n")
}
int main(){
  return 0;
}
然後再走一遍生成.class、.h 、so 然後執行
jni反呼叫java編譯:
gcc -o threadNew threadNew.c -I /usr/lib/jvm/java-1.8.0-openjdk/include -I /usr/lib/jvm/java-1.8.0-openjdk/include/linux -L /usr/lib/jvm/java-1.8.0- openjdk/jre/lib/amd64/server -ljvm -pthread
指定:export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:{libLubanThreadNative.so}
顯示:
I am java Thread !!
Finsh call method!

至此c呼叫java的已經完成(只是模擬)(其實C呼叫java的時候並不是呼叫jni反射呼叫的,而是用的C++的一個函式)
由上可知java thread的呼叫及反呼叫:
呼叫了一個start0方法,而start0方法又是一個native方法,native方法是由Hotspot提供的,並且呼叫OS pthread_create()
證實: java thread —-對應-—> OS thr