前言
在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
下載地址: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 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,而第四個引數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.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
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/205042.html
標籤:Java
