簡述Java中進程與線程的關系_動力節(jié)點Java學院整理
概述
進程與線程,本質意義上說, 是操作系統(tǒng)的調度單位,可以看成是一種操作系統(tǒng) “資源” 。Java 作為與平臺無關的編程語言,必然會對底層(操作系統(tǒng))提供的功能進行進一步的封裝,以平臺無關的編程接口供程序員使用,進程與線程作為操作系統(tǒng)核心概念的一部分無疑亦是如此。在 Java 語言中,對進程和線程的封裝,分別提供了 Process 和 Thread 相關的一些類。本文首先簡單的介紹如何使用這些類來創(chuàng)建進程和線程,然后著重介紹這些類是如何和操作系統(tǒng)本地進程線程相對應的,給出了 Java 虛擬機對于這些封裝類的概要性的實現(xiàn);同時由于 Java 的封裝也隱藏了底層的一些概念和可操作性,本文還對 Java 進程線程和本地進程線程做了一些簡單的比較,列出了使用 Java 進程、線程的一些限制和需要注意的問題。
Java 進程的建立方法
在 JDK 中,與進程有直接關系的類為 Java.lang.Process,它是一個抽象類。在 JDK 中也提供了一個實現(xiàn)該抽象類的 ProcessImpl 類,如果用戶創(chuàng)建了一個進程,那么肯定會伴隨著一個新的 ProcessImpl 實例。同時和進程創(chuàng)建密切相關的還有 ProcessBuilder,它是在 JDK1.5 中才開始出現(xiàn)的,相對于 Process 類來說,提供了便捷的配置新建進程的環(huán)境,目錄以及是否合并錯誤流和輸出流的方式。
Java.lang.Runtime.exec 方法和 Java.lang.ProcessBuilder.start 方法都可以創(chuàng)建一個本地的進程,然后返回代表這個進程的 Java.lang.Process 引用。
Runtime.exec 方法建立一個本地進程
該方法在 JDK1.5 中,可以接受 6 種不同形式的參數(shù)傳入。
Process exec(String command) Process exec(String [] cmdarray) Process exec(String [] cmdarrag, String [] envp) Process exec(String [] cmdarrag, String [] envp, File dir) Process exec(String cmd, String [] envp) Process exec(String command, String [] envp, File dir)
他們主要的不同在于傳入命令參數(shù)的形式,提供的環(huán)境變量以及定義執(zhí)行目錄。
ProcessBuilder.start 方法來建立一個本地的進程
如果希望在新創(chuàng)建的進程中使用當前的目錄和環(huán)境變量,則不需要任何配置,直接將命令行和參數(shù)傳入 ProcessBuilder 中,然后調用 start 方法,就可以獲得進程的引用。
Process p = new ProcessBuilder("command", "param").start();
也可以先配置環(huán)境變量和工作目錄,然后創(chuàng)建進程。
ProcessBuilder pb = new ProcessBuilder("command", "param1", "param2");
Map<String, String> env = pb.environment();
env.put("VAR", "Value");
pb.directory("Dir");
Process p = pb.start();
可以預先配置 ProcessBuilder 的屬性是通過 ProcessBuilder 創(chuàng)建進程的最大優(yōu)點。而且可以在后面的使用中隨著需要去改變代碼中 pb 變量的屬性。如果后續(xù)代碼修改了其屬性,那么會影響到修改后用 start 方法創(chuàng)建的進程,對修改之前創(chuàng)建的進程實例沒有影響。
JVM 對進程的實現(xiàn)
在 JDK 的代碼中,只提供了 ProcessImpl 類來實現(xiàn) Process 抽象類。其中引用了 native 的 create, close, waitfor, destory 和 exitValue 方法。在 Java 中,native 方法是依賴于操作系統(tǒng)平臺的本地方法,它的實現(xiàn)是用 C/C++ 等類似的底層語言實現(xiàn)。我們可以在 JVM 的源代碼中找到對應的本地方法,然后對其進行分析。JVM 對進程的實現(xiàn)相對比較簡單,以 Windows 下的 JVM 為例。在 JVM 中,將 Java 中調用方法時的傳入的參數(shù)傳遞給操作系統(tǒng)對應的方法來實現(xiàn)相應的功能。如表 1
表 1. JDK 中 native 方法與 Windows API 的對應關系

以 create 方法為例,我們看一下它是如何和系統(tǒng) API 進行連接的。
在 ProcessImple 類中,存在 native 的 create 方法,其參數(shù)如下:
private native long create(String cmdstr, String envblock, String dir, boolean redirectErrorStream, FileDescriptor in_fd, FileDescriptor out_fd, FileDescriptor err_fd) throws IOException;
在 JVM 中對應的本地方法如代碼清單 1 所示 。
清單 1
JNIEXPORT jlong JNICALL
Java_Java_lang_ProcessImpl_create(JNIEnv *env, jobject process,
jstring cmd,
jstring envBlock,
jstring dir,
jboolean redirectErrorStream,
jobject in_fd,
jobject out_fd,
jobject err_fd)
{
/* 設置內部變量值 */
……
/* 建立輸入、輸出以及錯誤流管道 */
if (!(CreatePipe(&inRead, &inWrite, &sa, PIPE_SIZE) &&
CreatePipe(&outRead, &outWrite, &sa, PIPE_SIZE) &&
CreatePipe(&errRead, &errWrite, &sa, PIPE_SIZE))) {
throwIOException(env, "CreatePipe failed");
goto Catch;
}
/* 進行參數(shù)格式的轉換 */
pcmd = (LPTSTR) JNU_GetStringPlatformChars(env, cmd, NULL);
……
/* 調用系統(tǒng)提供的方法,建立一個 Windows 的進程 */
ret = CreateProcess(
0, /* executable name */
pcmd, /* command line */
0, /* process security attribute */
0, /* thread security attribute */
TRUE, /* inherits system handles */
processFlag, /* selected based on exe type */
penvBlock, /* environment block */
pdir, /* change to the new current directory */
&si, /* (in) startup information */
&pi); /* (out) process information */
…
/* 拿到新進程的句柄 */
ret = (jlong)pi.hProcess;
…
/* 最后返回該句柄 */
return ret;
}
可以看到在創(chuàng)建一個進程的時候,調用 Windows 提供的 CreatePipe 方法建立輸入,輸出和錯誤管道,同時將用戶通過 Java 傳入的參數(shù)轉換為操作系統(tǒng)可以識別的 C 語言的格式,然后調用 Windows 提供的創(chuàng)建系統(tǒng)進程的方式,創(chuàng)建一個進程,同時在 JAVA 虛擬機中保存了這個進程對應的句柄,然后返回給了 ProcessImpl 類,但是該類將返回句柄進行了隱藏。也正是 Java 跨平臺的特性體現(xiàn),JVM 盡可能的將和操作系統(tǒng)相關的實現(xiàn)細節(jié)進行了封裝,并隱藏了起來。
同樣,在用戶調用 close、waitfor、destory 以及 exitValue 方法以后, JVM 會首先取得之前保存的該進程在操作系統(tǒng)中的句柄,然后通過調用操作系統(tǒng)提供的接口對該進程進行操作。通過這種方式來實現(xiàn)對進程的操作。
在其它平臺下也是用類似的方式實現(xiàn)的,不同的是調用的對應平臺的 API 會有所不同。
Java 進程與操作系統(tǒng)進程
通過上面對 Java 進程的分析,其實它在實現(xiàn)上就是創(chuàng)建了操作系統(tǒng)的一個進程,也就是每個 JVM 中創(chuàng)建的進程都對應了操作系統(tǒng)中的一個進程。但是,Java 為了給用戶更好的更方便的使用,向用戶屏蔽了一些與平臺相關的信息,這為用戶需要使用的時候,帶來了些許不便。
在使用 C/C++ 創(chuàng)建系統(tǒng)進程的時候,是可以獲得進程的 PID 值的,可以直接通過該 PID 去操作相應進程。但是在 JAVA 中,用戶只能通過實例的引用去進行操作,當該引用丟失或者無法取得的時候,就無法了解任何該進程的信息。
當然,Java 進程在使用的時候還有些要注意的事情:
1. Java 提供的輸入輸出的管道容量是十分有限的,如果不及時讀取會導致進程掛起甚至引起死鎖。
2. 當創(chuàng)建進程去執(zhí)行 Windows 下的系統(tǒng)命令時,如:dir、copy 等。需要運行 windows 的命令解釋器,command.exe/cmd.exe,這依賴于 windows 的版本,這樣才可以運行系統(tǒng)的命令。
3. 對于 Shell 中的管道 ‘ | '命令,各平臺下的重定向命令符 ‘ > ',都無法通過命令參數(shù)直接傳入進行實現(xiàn),而需要在 Java 代碼中做一些處理,如定義新的流來存儲標準輸出,等等問題。
總之,Java 中對操作系統(tǒng)的進程進行了封裝,屏蔽了操作系統(tǒng)進程相關的信息。同時,在使用 Java 提供創(chuàng)建進程運行本地命令的時候,需要小心使用。
一般而言,使用進程是為了執(zhí)行某項任務,而現(xiàn)代操作系統(tǒng)對于執(zhí)行任務的計算資源的配置調度一般是以線程為對象(早期的類 Unix 系統(tǒng)因為不支持線程,所以進程也是調度單位,但那是比較輕量級的進程,在此不做深入討論)。創(chuàng)建一個進程,操作系統(tǒng)實際上還是會為此創(chuàng)建相應的線程以運行一系列指令。特別地,當一個任務比較龐大復雜,可能需要創(chuàng)建多個線程以實現(xiàn)邏輯上并發(fā)執(zhí)行的時候,線程的作用更為明顯。因而我們有必要深入了解 Java 中的線程,以避免可能出現(xiàn)的問題。本文下面的內容即是呈現(xiàn) Java 線程的創(chuàng)建方式以及它與操作系統(tǒng)線程的聯(lián)系與區(qū)別。
Java 創(chuàng)建線程的方法
實際上,創(chuàng)建線程最重要的是提供線程函數(shù)(回調函數(shù)),該函數(shù)作為新創(chuàng)建線程的入口函數(shù),實現(xiàn)自己想要的功能。Java 提供了兩種方法來創(chuàng)建一個線程:
1. 繼承 Thread 類
class MyThread extends Thread{
public void run() {
System.out.println("My thread is started.");
}
}
實現(xiàn)該繼承類的 run 方法,然后就可以創(chuàng)建這個子類的對象,調用 start 方法即可創(chuàng)建一個新的線程:
MyThread myThread = new MyThread(); myThread.start();
2. 實現(xiàn) Runnable 接口
class MyRunnable implements Runnable{
public void run() {
System.out.println("My runnable is invoked.");
}
}
實現(xiàn) Runnable 接口的類的對象可以作為一個參數(shù)傳遞到創(chuàng)建的 Thread 對象中,同樣調用 Thread#start 方法就可以在一個新的線程中運行 run 方法中的代碼了。
Thread myThread = new Thread( new MyRunnable()); myThread.start();
可以看到,不管是用哪種方法,實際上都是要實現(xiàn)一個 run 方法的。 該方法本質是上一個回調方法。由 start 方法新創(chuàng)建的線程會調用這個方法從而執(zhí)行需要的代碼。 從后面可以看到,run 方法并不是真正的線程函數(shù),只是被線程函數(shù)調用的一個 Java 方法而已,和其他的 Java 方法沒有什么本質的不同。
Java 線程的實現(xiàn)
從概念上來說,一個 Java 線程的創(chuàng)建根本上就對應了一個本地線程(native thread)的創(chuàng)建,兩者是一一對應的。 問題是,本地線程執(zhí)行的應該是本地代碼,而 Java 線程提供的線程函數(shù)是 Java 方法,編譯出的是 Java 字節(jié)碼,所以可以想象的是, Java 線程其實提供了一個統(tǒng)一的線程函數(shù),該線程函數(shù)通過 Java 虛擬機調用 Java 線程方法 , 這是通過 Java 本地方法調用來實現(xiàn)的。
以下是 Thread#start 方法的示例:
public synchronized void start() {
…
start0();
…
}
可以看到它實際上調用了本地方法 start0, 該方法的聲明如下:
private native void start0();
Thread 類有個 registerNatives 本地方法,該方法主要的作用就是注冊一些本地方法供 Thread 類使用,如 start0(),stop0() 等等,可以說,所有操作本地線程的本地方法都是由它注冊的 . 這個方法放在一個 static 語句塊中,這就表明,當該類被加載到 JVM 中的時候,它就會被調用,進而注冊相應的本地方法。
private static native void registerNatives();
static{
registerNatives();
}
本地方法 registerNatives 是定義在 Thread.c 文件中的。Thread.c 是個很小的文件,定義了各個操作系統(tǒng)平臺都要用到的關于線程的公用數(shù)據(jù)和操作,如代碼清單 2 所示。
清單 2
JNIEXPORT void JNICALL
Java_Java_lang_Thread_registerNatives (JNIEnv *env, jclass cls){
(*env)->RegisterNatives(env, cls, methods, ARRAY_LENGTH(methods));
}
static JNINativeMethod methods[] = {
{"start0", "()V",(void *)&JVM_StartThread},
{"stop0", "(" OBJ ")V", (void *)&JVM_StopThread},
{"isAlive","()Z",(void *)&JVM_IsThreadAlive},
{"suspend0","()V",(void *)&JVM_SuspendThread},
{"resume0","()V",(void *)&JVM_ResumeThread},
{"setPriority0","(I)V",(void *)&JVM_SetThreadPriority},
{"yield", "()V",(void *)&JVM_Yield},
{"sleep","(J)V",(void *)&JVM_Sleep},
{"currentThread","()" THD,(void *)&JVM_CurrentThread},
{"countStackFrames","()I",(void *)&JVM_CountStackFrames},
{"interrupt0","()V",(void *)&JVM_Interrupt},
{"isInterrupted","(Z)Z",(void *)&JVM_IsInterrupted},
{"holdsLock","(" OBJ ")Z",(void *)&JVM_HoldsLock},
{"getThreads","()[" THD,(void *)&JVM_GetAllThreads},
{"dumpThreads","([" THD ")[[" STE, (void *)&JVM_DumpThreads},
};
到此,可以容易的看出 Java 線程調用 start 的方法,實際上會調用到 JVM_StartThread 方法,那這個方法又是怎樣的邏輯呢。實際上,我們需要的是(或者說 Java 表現(xiàn)行為)該方法最終要調用 Java 線程的 run 方法,事實的確如此。 在 jvm.cpp 中,有如下代碼段:
JVM_ENTRY(void, JVM_StartThread(JNIEnv* env, jobject jthread)) … native_thread = new JavaThread(&thread_entry, sz); …
這里JVM_ENTRY是一個宏,用來定義JVM_StartThread 函數(shù),可以看到函數(shù)內創(chuàng)建了真正的平臺相關的本地線程,其線程函數(shù)是 thread_entry,如清單 3 所示。
清單 3
static void thread_entry(JavaThread* thread, TRAPS) {
HandleMark hm(THREAD);
Handle obj(THREAD, thread->threadObj());
JavaValue result(T_VOID);
JavaCalls::call_virtual(&result,obj,
KlassHandle(THREAD,SystemDictionary::Thread_klass()),
vmSymbolHandles::run_method_name(),
vmSymbolHandles::void_method_signature(),THREAD);
}
可以看到調用了 vmSymbolHandles::run_method_name 方法,這是在 vmSymbols.hpp 用宏定義的:
class vmSymbolHandles: AllStatic {
…
template(run_method_name,"run")
…
}
至于 run_method_name 是如何聲明定義的,因為涉及到很繁瑣的代碼細節(jié),本文不做贅述。感興趣的讀者可以自行查看 JVM 的源代碼。
圖 1. Java 線程創(chuàng)建調用關系圖
綜上所述,Java 線程的創(chuàng)建調用過程如 圖 1 所示,首先 , Java 線程的 start 方法會創(chuàng)建一個本地線程(通過調用 JVM_StartThread),該線程的線程函數(shù)是定義在 jvm.cpp 中的 thread_entry,由其再進一步調用 run 方法??梢钥吹?Java 線程的 run 方法和普通方法其實沒有本質區(qū)別,直接調用 run 方法不會報錯,但是卻是在當前線程執(zhí)行,而不會創(chuàng)建一個新的線程。
Java 線程與操作系統(tǒng)線程
從上我們知道,Java 線程是建立在系統(tǒng)本地線程之上的,是另一層封裝,其面向 Java 開發(fā)者提供的接口存在以下的局限性:
線程返回值
Java 沒有提供方法來獲取線程的退出返回值。實際上,線程可以有退出返回值,它一般被操作系統(tǒng)存儲在線程控制結構中 (TCB),調用者可以通過檢測該值來確定線程是正常退出還是異常終止。
線程的同步
Java 提供方法 Thread#Join()來等待一個線程結束,一般情況這就足夠了,但一種可能的情況是,需要等待在多個線程上(比如任意一個線程結束或者所有線程結束才會返回),循環(huán)調用每個線程的 Join 方法是不可行的,這可能導致很奇怪的同步問題。
線程的 ID
Java 提供的方法 Thread#getID()返回的是一個簡單的計數(shù) ID,其實和操作系統(tǒng)線程的 ID 沒有任何關系。
線程運行時間統(tǒng)計,Java 沒有提供方法來獲取線程中某段代碼的運行時間的統(tǒng)計結果。雖然可以自行使用計時的方法來實現(xiàn)(獲取運行開始和結束的時間,然后相減 ),但由于存在多線程調度方法的原因,無法獲取線程實際使用的 CPU 運算時間,因而必然是不準確的。
總結
本文通過對 Java 進程和線程的分析,可以看出 Java 對這兩種操作系統(tǒng) “資源” 進行了封裝,使得開發(fā)人員只需關注如何使用這兩種 “資源” ,而不必過多的關心細節(jié)。這樣的封裝一方面降低了開發(fā)人員的工作復雜度,提高了工作效率;另一方面由于封裝屏蔽了操作系統(tǒng)本身的一些特性,因而在使用 Java 進程線程時有了某些限制,這是封裝不可避免的問題。語言的演化本就是決定需要什么不需要什么的過程,相信隨著 Java 的不斷發(fā)展,封裝的功能子集的必然越來越完善。
相關文章
Java?方法(方法的定義,可變參數(shù),參數(shù)的傳遞問題,方法重載,方法簽名)
這篇文章主要介紹了Java?方法(方法的定義,可變參數(shù),參數(shù)的傳遞問題,方法重載,方法簽名),文章圍繞主題展開詳細的內容介紹,具有一定的參考價值,感興趣的小伙伴可以參考一下2022-09-09
Springboot自帶定時任務實現(xiàn)動態(tài)配置Cron參數(shù)方式
這篇文章主要介紹了Springboot自帶定時任務實現(xiàn)動態(tài)配置Cron參數(shù)方式,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2021-11-11
springcloud?eureka切換nacos的配置方法
這篇文章主要介紹了springcloud?eureka切換nacos,本文給大家介紹的非常詳細,對大家的學習或工作具有一定的參考借鑒價值,需要的朋友可以參考下2023-01-01
Java的ThreadPoolExecutor業(yè)務線程池詳細解析
這篇文章主要介紹了Java線程池ThreadPoolExecutor詳細解析,任務剛開始進來的時候就創(chuàng)建核心線程,核心線程滿了會把任務放到阻塞隊列,阻塞隊列滿了之后才會創(chuàng)建空閑線程,達到最大線程數(shù)之后,再有任務進來,就只能執(zhí)行拒絕策略了,需要的朋友可以參考下2024-01-01

