前言
在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 __cplusplusextern "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.JniTesthello 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
下載地址:http://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 JNICALLJava_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 JNICALLJVM_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_END3)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,而第四個參數arg是start_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_point是thread_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.jnilibjavac JniTest.javajava 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
原文連結:https://www.cnblogs.com/tera/p/13937611.html
如果覺得本文對你有幫助,可以轉發關注支持一下