Java中的進程與線程程序員
進程與線程,本質意義上說, 是操做系統的調度單位,能夠當作是一種操做系統 「資源」 。Java 做爲與平臺無關的編程語言,必然會對底層(操做系統)提供的功能進行進一步的封裝,以平臺無關的編程接口供程序員使用,進程與線程做爲操做系統核心概念的一部分無疑亦是如此。在 Java 語言中,對進程和線程的封裝,分別提供了 Process 和 Thread 相關的一些類。本文首先簡單的介紹如何使用這些類來建立進程和線程,而後着重介紹這些類是如何和操做系統本地進程線程相對應的,給出了 Java 虛擬機對於這些封裝類的概要性的實現;同時因爲 Java 的封裝也隱藏了底層的一些概念和可操做性,本文還對 Java 進程線程和本地進程線程作了一些簡單的比較,列出了使用 Java 進程、線程的一些限制和須要注意的問題。編程
在 JDK 中,與進程有直接關係的類爲 Java.lang.Process,它是一個抽象類。在 JDK 中也提供了一個實現該抽象類的 ProcessImpl 類,若是用戶建立了一個進程,那麼確定會伴隨着一個新的 ProcessImpl 實例。同時和進程建立密切相關的還有 ProcessBuilder,它是在 JDK1.5 中才開始出現的,相對於 Process 類來講,提供了便捷的配置新建進程的環境,目錄以及是否合併錯誤流和輸出流的方式。 Java.lang.Runtime.exec 方法和 Java.lang.ProcessBuilder.start 方法均可以建立一個本地的進程,而後返回表明這個進程的 Java.lang.Process 引用。windows
該方法在 JDK1.5 中,能夠接受 6 種不一樣形式的參數傳入。多線程
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)
他們主要的不一樣在於傳入命令參數的形式,提供的環境變量以及定義執行目錄。併發
若是但願在新建立的進程中使用當前的目錄和環境變量,則不須要任何配置,直接將命令行和參數傳入 ProcessBuilder 中,而後調用 start 方法,就能夠得到進程的引用。jvm
Process p = new ProcessBuilder("command", "param").start();
也能夠先配置環境變量和工做目錄,而後建立進程。編程語言
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 建立進程的最大優勢。並且能夠在後面的使用中隨着須要去改變代碼中 pb 變量的屬性。若是後續代碼修改了其屬性,那麼會影響到修改後用 start 方法建立的進程,對修改以前建立的進程實例沒有影響。函數
在 JDK 的代碼中,只提供了 ProcessImpl 類來實現 Process 抽象類。其中引用了 native 的 create, close, waitfor, destory 和 exitValue 方法。在 Java 中,native 方法是依賴於操做系統平臺的本地方法,它的實現是用 C/C++ 等相似的底層語言實現。咱們能夠在 JVM 的源代碼中找到對應的本地方法,而後對其進行分析。JVM 對進程的實現相對比較簡單,以 Windows 下的 JVM 爲例。在 JVM 中,將 Java 中調用方法時的傳入的參數傳遞給操做系統對應的方法來實現相應的功能。ui
以 create 方法爲例,咱們看一下它是如何和系統 API 進行鏈接的。 在 ProcessImple 類中,存在 native 的 create 方法,其參數以下:spa
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 所示 。
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; } /* 進行參數格式的轉換 */ pcmd = (LPTSTR) JNU_GetStringPlatformChars(env, cmd, NULL); …… /* 調用系統提供的方法,創建一個 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; }
能夠看到在建立一個進程的時候,調用 Windows 提供的 CreatePipe 方法創建輸入,輸出和錯誤管道,同時將用戶經過 Java 傳入的參數轉換爲操做系統能夠識別的 C 語言的格式,而後調用 Windows 提供的建立系統進程的方式,建立一個進程,同時在 JAVA 虛擬機中保存了這個進程對應的句柄,而後返回給了 ProcessImpl 類,可是該類將返回句柄進行了隱藏。也正是 Java 跨平臺的特性體現,JVM 儘量的將和操做系統相關的實現細節進行了封裝,並隱藏了起來。 一樣,在用戶調用 close、waitfor、destory 以及 exitValue 方法之後, JVM 會首先取得以前保存的該進程在操做系統中的句柄,而後經過調用操做系統提供的接口對該進程進行操做。經過這種方式來實現對進程的操做。 在其它平臺下也是用相似的方式實現的,不一樣的是調用的對應平臺的 API 會有所不一樣。
經過上面對 Java 進程的分析,其實它在實現上就是建立了操做系統的一個進程,也就是每一個 JVM 中建立的進程都對應了操做系統中的一個進程。可是,Java 爲了給用戶更好的更方便的使用,向用戶屏蔽了一些與平臺相關的信息,這爲用戶須要使用的時候,帶來了些許不便。 在使用 C/C++ 建立系統進程的時候,是能夠得到進程的 PID 值的,能夠直接經過該 PID 去操做相應進程。可是在 JAVA 中,用戶只能經過實例的引用去進行操做,當該引用丟失或者沒法取得的時候,就沒法瞭解任何該進程的信息。
固然,Java 進程在使用的時候還有些要注意的事情:
總之,Java 中對操做系統的進程進行了封裝,屏蔽了操做系統進程相關的信息。同時,在使用 Java 提供建立進程運行本地命令的時候,須要當心使用。
通常而言,使用進程是爲了執行某項任務,而現代操做系統對於執行任務的計算資源的配置調度通常是以線程爲對象(早期的類 Unix 系統由於不支持線程,因此進程也是調度單位,但那是比較輕量級的進程,在此不作深刻討論)。建立一個進程,操做系統實際上仍是會爲此建立相應的線程以運行一系列指令。特別地,當一個任務比較龐大複雜,可能須要建立多個線程以實現邏輯上併發執行的時候,線程的做用更爲明顯。於是咱們有必要深刻了解 Java 中的線程,以免可能出現的問題。本文下面的內容便是呈現 Java 線程的建立方式以及它與操做系統線程的聯繫與區別。
實際上,建立線程最重要的是提供線程函數(回調函數),該函數做爲新建立線程的入口函數,實現本身想要的功能。Java 提供了兩種方法來建立一個線程:
不論是用哪一種方法,實際上都是要實現一個 run 方法的。 該方法本質是上一個回調方法。由 start 方法新建立的線程會調用這個方法從而執行須要的代碼。 從後面能夠看到,run 方法並非真正的線程函數,只是被線程函數調用的一個 Java 方法而已,和其餘的 Java 方法沒有什麼本質的不一樣。
從概念上來講,一個 Java 線程的建立根本上就對應了一個本地線程(native thread)的建立,二者是一一對應的。 問題是,本地線程執行的應該是本地代碼,而 Java 線程提供的線程函數是 Java 方法,編譯出的是 Java 字節碼,因此能夠想象的是, Java 線程其實提供了一個統一的線程函數,該線程函數經過 Java 虛擬機調用 Java 線程方法 , 這是經過 Java 本地方法調用來實現的。
如下是 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 是個很小的文件,定義了各個操做系統平臺都要用到的關於線程的公用數據和操做,如代碼清單 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 表現行爲)該方法最終要調用 Java 線程的 run 方法,事實的確如此。 在 jvm.cpp 中,有以下代碼段:
JVM_ENTRY(void, JVM_StartThread(JNIEnv* env, jobject jthread)) … native_thread = new JavaThread(&thread_entry, sz); …
這裏JVM_ENTRY是一個宏,用來定義JVM_StartThread 函數,能夠看到函數內建立了真正的平臺相關的本地線程,其線程函數是 thread_entry,如清單 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 是如何聲明定義的,由於涉及到很繁瑣的代碼細節,本文不作贅述。感興趣的讀者能夠自行查看 JVM 的源代碼。 圖 1. Java 線程建立調用關係圖
綜上所述,Java 線程的建立調用過程如 圖 1 所示,首先 , Java 線程的 start 方法會建立一個本地線程(經過調用 JVM_StartThread),該線程的線程函數是定義在 jvm.cpp 中的 thread_entry,由其再進一步調用 run 方法。能夠看到 Java 線程的 run 方法和普通方法其實沒有本質區別,直接調用 run 方法不會報錯,可是倒是在當前線程執行,而不會建立一個新的線程。
從上咱們知道,Java 線程是創建在系統本地線程之上的,是另外一層封裝,其面向 Java 開發者提供的接口存在如下的侷限性:
本文經過對 Java 進程和線程的分析,能夠看出 Java 對這兩種操做系統 「資源」 進行了封裝,使得開發人員只需關注如何使用這兩種 「資源」 ,而沒必要過多的關心細節。這樣的封裝一方面下降了開發人員的工做複雜度,提升了工做效率;另外一方面因爲封裝屏蔽了操做系統自己的一些特性,於是在使用 Java 進程線程時有了某些限制,這是封裝不可避免的問題。語言的演化本就是決定須要什麼不須要什麼的過程,相信隨着 Java 的不斷髮展,封裝的功能子集的必然愈來愈完善。
========END========