JNI-Thread中start方法的調用與run方法的回調分析

前言

在java編程中,執行緒Thread是我們經常使用的類。那麼創建一個Thread的本質究竟是什麼,本文就此問題作一個探索。

內容主要分為以下幾個部分

1.JNI機制的使用

2.Thread創建執行緒的底層調用分析

3.系統執行緒的使用

4.Thread中run方法的回調分析

5.實現一個jni的回調

1.JNI機制的基本使用

當我們new出一個Thread的時候,僅僅是創建了一個java層面的執行緒對象,而只有當Thread的start方法被調用的時候,一個執行緒才真正開始執行了。所以start方法是我們關注的目標

查看Thread類的start方法

public synchronized void start() {
    if (threadStatus != 0)
        throw new IllegalThreadStateException();
    group.add(this);

    boolean started = false;
    try {
        start0();
        started = true;
    } finally {
        try {
            if (!started) {
                group.threadStartFailed(this);
            }
        } catch (Throwable ignore) {
        }
    }
}

Start方法本身並不複雜,其核心是start0(),真正地將執行緒啟動起來。

接著我們查看start0()方法

private native void start0();

可以看到這是一個native方法,這裡我們需要先解釋一下什麼是native方法。

眾所周知java是一個跨平台的語言,用java編譯的程式碼可以運行在任何安裝了jvm的系統上。然而各個系統的底層實現肯定是有區別的,為了使java可以跨平台,於是jvm提供了叫java native interface(JNI)的機制。當java需要使用到一些系統方法時,由jvm幫我們去調用系統底層,而java本身只需要告知jvm需要做的事情,即調用某個native方法即可。

例如,當我們需要啟動一個執行緒時,無論在哪個平台上,我們調用的都是start0方法,由jvm根據不同的作業系統,去調用相應系統底層方法,幫我們真正地啟動一個執行緒。因此這就像是jvm為我們提供了一個可以作業系統底層方法的介面,即JNI,java本地介面。

在深入查看start0()方法之前,我們先實現一個自己的JNI方法,這樣才能更好地理解start0()方法是如何調用到系統層面的native方法。

首先我們先定義一個簡單的java類

package cn.tera.jni;

public class JniTest {
    public native void jniHello();

    public static void main(String[] args) {
        JniTest jni = new JniTest();
        jni.jniHello();
    }
}

在這個類中,我們定義了一個jniHello的native方法,然後在main方法中對其進行調用。

接著我們調用javac命令將其編譯成一個class文件,但和平時不同,我們需要加一個-h參數,生成一個頭文件

javac -h . JniTest.java

注意-h後面有一個.,意思是生成的頭文件,存放在當前目錄

這時我們可以看到在當前目錄下生成了2個新文件

JniTest.class:JniTest類的位元組碼

cn_tera_jni_JniTest.h:.h頭文件,這個文件是C和C++中所需要用到的,其中定義了方法的參數、返回類型等,但不包含實現,類似java中的介面,而java程式碼正是通過這個「介面」找到真正需要執行的方法。

我們查看該.h文件,其中就包含了jniHello方法的定義,當然需要注意到的是,這裡的方法名和.h文件本身的命名是jni根據我們類的包名和類名確定出來的,不能修改。

/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class cn_tera_jni_JniTest */

#ifndef _Included_cn_tera_jni_JniTest
#define _Included_cn_tera_jni_JniTest
#ifdef __cplusplus
extern "C" {
#endif
/*
 * Class:     cn_tera_jni_JniTest
 * Method:    jniHello
 * Signature: ()V
 */
JNIEXPORT void JNICALL Java_cn_tera_jni_JniTest_jniHello
  (JNIEnv *, jobject);

#ifdef __cplusplus
}
#endif
#endif

既然我們有了.h頭文件,那麼自然需要.c或者.cpp的定義實際執行內容的文件,即介面的實現。

我們希望該方法簡單地輸出一個”hello jni”,於是定義如下方法,並將其保存在cn_tera_jni_JniTest.c文件中(這裡文件名不需要一致,不過為了可維護性,我們應當定義一致)

#include "cn_tera_jni_JniTest.h"

JNIEXPORT void JNICALL Java_cn_tera_jni_JniTest_jniHello(JNIEnv *env, jobject c1){
    printf("hello jni\n");
}

在該文件中,引入了之前生成.h文件(類似於java指定了類實現了哪個介面),並且定義了簽名完全一致的Java_cn_tera_jni_JniTest_jniHello方法,此時我們已經有了「介面」和「實現」,接著生成動態鏈接庫即可。

Mac系統運行命令:

gcc -dynamiclib -I /Library/Java/JavaVirtualMachines/jdk1.8.0_241.jdk/Contents/Home/include cn_tera_jni_JniTest.c -o libJniTest.jnilib 

Linux系統運行命令:

gcc -shared -I /usr/lib/jdk1.8.0_241/include cn_tera_jni_JniTest.c -o libJniTest.so

-dynamiclib、-shared:表示我們需要生成一個動態鏈接庫

-I:之前在.h頭文件中我們需要引入jni.h,而該文件位與jdk的目錄下,這裡-I就是include的意思

-o:表示輸出的文件

​ 在Mac系統下,鏈接庫的擴展名為jnilib,命名的格式為libXXX.jnilib

​ 在Linux系統下,鏈接庫擴展名為so,命名格式為libXXX.so

​ 其中的XXX是在運行時載入動態庫時用到的名字

此時在目錄下就會多出一個libJniTest.jnilib或者libJniTest.so的動態鏈接庫。

最後我們回到一開始的java文件中,引入該庫即可。修改JniTest.java

package cn.tera.jni;

public class JniTest {
    static {
        //設置查找路徑為當前項目路徑
        System.setProperty("java.library.path", ".");
        //載入動態庫的名稱
        System.loadLibrary("JniTest");
    }

    public native void jniHello();

    public static void main(String[] args) {
        JniTest jni = new JniTest();
        jni.jniHello();
    }
}

重新編譯.class文件,記得將其放到./cn/tera/jni目錄下(包名是啥,目錄就是啥),然後執行即可。

java cn.tera.jni.JniTest
hello jni

此時我們先總結一下JNI的基本使用順序

1)在.java文件中定義native方法

2)生成相應的.h頭文件(即介面)

3)編寫相應的.c或.cpp文件(即實現)

4)將介面和實現鏈接到一起,生成動態鏈接庫

5)在.java中引入該庫,即可調用native方法

2.Thread創建執行緒的底層調用分析

了解了jni的基本使用流程之後,我們回到Thread的start0方法

為了探究start0()方法的原理,自然需要看看jvm在幕後為我們做了什麼。

首先我們需要下載jdk和jvm的源碼,因為openjdk和oraclejdk差別很小,而openjdk是開源的,所以我們以openjdk的程式碼為參考,版本是jdk8

下載地址://hg.openjdk.java.net/jdk8

因為C和C++的程式碼對於java程式設計師來說比較晦澀難懂,所以在下方展示源碼的時候我只會貼出我們關心的重點程式碼,其餘的部分就省略了

在jdk源碼的目錄src/java.base/share/native/libjava目錄下能看到Thread.c文件,對應的是jni中的「實現」

#include "jni.h"
#include "jvm.h"

#include "java_lang_Thread.h"
...
static JNINativeMethod methods[] = {
    {"start0",           "()V",        (void *)&JVM_StartThread},
    ...
};
JNIEXPORT void JNICALL
Java_java_lang_Thread_registerNatives(JNIEnv *env, jclass cls)
{
    (*env)->RegisterNatives(env, cls, methods, ARRAY_LENGTH(methods));
}

按照之前我們自己定義的jni實現,該文件中應當有一個Java_java_lang_Thread_start0的方法定義,然而其中實際上只有一個Java_java_lang_Thread_registerNatives的方法定義,對應的正是Thread.java中的registerNatives方法:

class Thread implements Runnable {
    private static native void registerNatives();
    static {
        registerNatives();
    }
    ...
}

由此我們可以發現,Thread類在實現jni的時候並非是將每一個native方法都直接定義在自己的頭文件中,而是通過一個registerNatives方法動態註冊的,而註冊所需要的資訊都被定義在了methods數組中,包括方法名、方法簽名和介面方法,介面方法的定義被統一放到了jvm.h中(#include “jvm.h”)。這個時候該jni介面方法的名字就不再受到固定格式限制了。這個機制以後用單獨的文章來解釋,現在先關心Thread的本質。

接下去我會按照調用鏈從上至下的順序列出文件和方法

1)jvm.h,hotspot目錄src/share/vm/prims

既然start0方法的介面方法被定義在jvm.h中,那麼我們先查看jvm.h,就可以找到JVM_StartThread的定義了:

JNIEXPORT void JNICALL
JVM_StartThread(JNIEnv *env, jobject thread);

2)jvm.cpp,hotspot目錄src/share/vm/prims

接著我們查看jvm.cpp,這裡能看到JVM_StartThread的具體實現,關鍵點是通過創建一個JavaThread類創建執行緒,注意這裡JavaThread是C++級別的執行緒:

JVM_ENTRY(void, JVM_StartThread(JNIEnv* env, jobject jthread))
  JVMWrapper("JVM_StartThread");
  JavaThread *native_thread = NULL;
  bool throw_illegal_thread_state = false;

  {
      ...
      /**
       * 創建一個C++級別的執行緒
       */
      native_thread = new JavaThread(&thread_entry, sz);
      ...
  }
  ...
JVM_END

3)thread.cpp,hotspot目錄src/share/vm/runtime

查看thread.cpp,可以看到JavaThread的構造函數,其中創建了一個系統執行緒:

JavaThread::JavaThread(ThreadFunction entry_point, size_t stack_sz) :
                       Thread()
{
  ...
  /**
   * 創建系統執行緒
   */
  os::create_thread(this, thr_type, stack_sz);
}

4)os_linux.cpp,hotspot目錄src/os/linux/vm

我們能在hotspot源碼目錄的src/os下找到不同系統的方法,我們以linux系統為例。

查看os_linux.cpp,找到create_thread方法:

bool os::create_thread(Thread* thread, ThreadType thr_type,
                       size_t req_stack_size) {
    ...
    pthread_t tid;
    int ret = pthread_create(&tid, &attr, (void* (*)(void*)) thread_native_entry, thread);
    ...
}

這個pthread_create方法就是最終創建系統執行緒的底層方法

因此java執行緒start方法的本質其實就是通過jni機制,最終調用系統底層的pthread_create方法,創建了一個系統執行緒,因此java執行緒和系統執行緒是一個一對一的關係

3.系統執行緒的使用

接著我們來簡單使用一下這個創建執行緒的方法。創建如下的.c文件,在main方法中創建一個執行緒,並讓2個執行緒不斷列印一些文案

#include <pthread.h>
#include <stdio.h>

pthread_t pid;

void* thread_entity(void* arg){
    while (1) {
        printf("i am thread\n");
    }
}

int main(){
    pthread_create(&pid,NULL,thread_entity,NULL);
    while (1) {
        printf("i am main\n");
    }
    return 1;
}

編譯該文件

gcc threaddemo.c -o threaddemo.out

-o:編譯後的執行文件為threaddemo.out

運行該out文件後就能看到2個文案在不斷重複列印了,也就是成功通過pthread_create方法創建了一個系統級別的執行緒。

4.Thread中run方法的回調分析

到這裡我們的探究並沒有結束,在java的Thread類中,我們會傳入一個執行我們指定任務的Runnable對象,在Thread的run()方法中調用。當java通過jni調用到pthread_create創建完系統執行緒後,又要如何回調java中的run方法呢?

前面的探究我們是從java層開始,從上往下找,此時我們要反過來,從下往上找了。

1)pthread_create

先看pthread_create方法本身,它接收4個參數,其中第三個參數start_routine是系統執行緒創建後需要執行的方法,就像前面我們創建的簡單示例中的thread_entity,而第四個參數argstart_routine方法需要的參數

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

2)os_linux.cpp

查看create_thread方法中調用pthread_create的程式碼,可以看到thread_native_entry就是系統執行緒所執行的方法,而thread則是傳遞給thread_native_entry的參數:

int ret = pthread_create(&tid, &attr, (void* (*)(void*)) thread_native_entry, thread);

查看thread_native_entry方法,它獲取的參數正是一個Thread,並調用其run()方法。注意這個Thread是C++級別的執行緒,來自於pthread_create方法的第4個參數:

static void *thread_native_entry(Thread *thread) {
  ...
  // call one more level start routine
  thread->run();
  ...
  return 0;
}

3)thread.cpp

查看JavaThread::run()方法,其主要的執行內容在thread_main_inner方法中:

void JavaThread::run() {
  /**
   * 主要的執行內容
   */
  thread_main_inner();
}

查看JavaThread::thread_main_inner()方法,其內部通過entry_point執行回調:

void JavaThread::thread_main_inner() {
  ...
  /**
   * 調用entry_point,執行外部傳入的方法,注意這裡的第一個參數是this
   * 即JavaThread對象本身,後面會看到該方法的定義
   */
  this->entry_point()(this, this);
  ...
}

查看JavaThread::JavaThread構造函數,可以看到這裡的entry_point是從外部傳入的

JavaThread::JavaThread(ThreadFunction entry_point, size_t stack_sz) :
                       Thread()
{
  ...
  set_entry_point(entry_point);
  ...
}

4)jvm.cpp

查看JVM_StartThread方法,可以看到傳給JavaThread的entry_pointthread_entry

JVM_ENTRY(void, JVM_StartThread(JNIEnv* env, jobject jthread))
  JVMWrapper("JVM_StartThread");
  JavaThread *native_thread = NULL;
  bool throw_illegal_thread_state = false;

  {
      ...
      /**
       * 傳給構造函數的entry_point是thread_entry
       */
      native_thread = new JavaThread(&thread_entry, sz);
      ...
  }
  ...
JVM_END

查看thread_entry,其中調用了JavaCalls::call_virtual去回調java級別的方法,其實看到它的方法簽名就能猜到個大概了

static void thread_entry(JavaThread* thread, TRAPS) {
  HandleMark hm(THREAD);
  /**
   * obj正是根據thread對象獲取到的,JavaThread在調用時會傳入this
   */
  Handle obj(THREAD, thread->threadObj());
  /**
   * 返回結果是void
   */
  JavaValue result(T_VOID);
  /**
   * 回調java級別的方法
   */
  JavaCalls::call_virtual(&result,//返回對象
                          //實例對象
                          obj,
                          //類
                          KlassHandle(THREAD, SystemDictionary::Thread_klass()),
                          //方法名
                          vmSymbols::run_method_name(),
                          //方法簽名
                          vmSymbols::void_method_signature(),
                          THREAD);
}

5)vmSymbols.hpp,hotspot目錄src/share/vm/classfiles

我們查看獲取方法名run_method_name和方法簽名void_method_signature的部分,可以看到正是獲取一個方法名為run,且不獲取任何參數,返回值為void的方法:

template(run_method_name,                           "run")
...
template(void_method_signature,                     "()V")

於是系統執行緒就能成功地回調java級別的run方法了!

這裡我整理了一下Thread的start0方法的調用上下游關係,方便大家整體把握

Thread.java

——–>jvm.cpp

​ ——–>thread.cpp

​ ——–>os_linux.cpp

​ ——–>pthread_create

5.實現一個jni的回調

最後我們嘗試自己實現一個簡單的方法回調。

修改一開始的JniTest.java,新增一個回調方法:

package cn.tera.jni;

public class JniTest {
    static {
        //設置查找路徑為當前項目路徑
        System.setProperty("java.library.path", ".");
        //載入動態庫的名稱
        System.loadLibrary("JniTest");
    }

    public native void jniHello();
    
    //新增一個回調方法
    public void callBack(){
        System.out.println("this is call back");
    }

    public static void main(String[] args) {
        JniTest jni = new JniTest();
        jni.jniHello();
    }
}

修改cn_tera_jni_JniTest.c文件,原先只是簡單輸出一個文案,現在改為回調java方法。可以看到這個流程和java中的反射機制非常相似:

#include "cn_tera_jni_JniTest.h"

JNIEXPORT void JNICALL Java_cn_tera_jni_JniTest_jniHello(JNIEnv *env, jobject c1){
    //獲取類資訊
    jclass thisClass = (*env)->GetObjectClass(env, c1);
    //根據方法名和簽名獲取方法的id
    jmethodID midCallBack = (*env)->GetMethodID(env, thisClass, "callback", "()V");
    //調用方法
    (*env)->CallVoidMethod(env, c1, midCallBack);
}

重新生成動態鏈接庫、編譯.class文件、運行:

gcc -dynamiclib -I /Library/Java/JavaVirtualMachines/jdk1.8.0_241.jdk/Contents/Home/include cn_tera_jni_JniTest.c -o libJniTest.jnilib
javac JniTest.java
java cn.tera.jni.JniTest

成功得到輸出結果:

this is call back

當然,對於有參數的、有返回結果的回調等,jni也提供了不同的調用方法,這個就不在本文中展開了,有興趣的同學可以自己去看下jni.h文件

還要提一點,上面展示的回調只是最基本的使用,而jvm中的官方回調方法,因為涉及到了java的父類繼承關係、方法句柄、vtable等等內容,這裡也就不展開了,同學們自己研究吧

最後,總結一下本文的內容

1.實現一個jni只需要4個東西,.java文件,.h頭文件(相當於介面),.c或.cpp文件(相當於實現),生成的動態鏈接庫。

2.java的Thread是通過jni機制最終調用到了系統底層的pthread_create方法創建執行緒的。

3.Thread的jni調用鏈:Thread.java->jvm.cpp->thread.cpp->os_linux.cpp->pthread_create

4.jni也可以回調java方法,從調用到回調完成了一個demo

Tags: