簡述Java中進程與線程的關(guān)系_動力節(jié)點Java學(xué)院整理
概述
進程與線程,本質(zhì)意義上說, 是操作系統(tǒng)的調(diào)度單位,可以看成是一種操作系統(tǒng) “資源” 。Java 作為與平臺無關(guān)的編程語言,必然會對底層(操作系統(tǒng))提供的功能進行進一步的封裝,以平臺無關(guān)的編程接口供程序員使用,進程與線程作為操作系統(tǒng)核心概念的一部分無疑亦是如此。在 Java 語言中,對進程和線程的封裝,分別提供了 Process 和 Thread 相關(guān)的一些類。本文首先簡單的介紹如何使用這些類來創(chuàng)建進程和線程,然后著重介紹這些類是如何和操作系統(tǒng)本地進程線程相對應(yīng)的,給出了 Java 虛擬機對于這些封裝類的概要性的實現(xiàn);同時由于 Java 的封裝也隱藏了底層的一些概念和可操作性,本文還對 Java 進程線程和本地進程線程做了一些簡單的比較,列出了使用 Java 進程、線程的一些限制和需要注意的問題。
Java 進程的建立方法
在 JDK 中,與進程有直接關(guān)系的類為 Java.lang.Process,它是一個抽象類。在 JDK 中也提供了一個實現(xiàn)該抽象類的 ProcessImpl 類,如果用戶創(chuàng)建了一個進程,那么肯定會伴隨著一個新的 ProcessImpl 實例。同時和進程創(chuàng)建密切相關(guān)的還有 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)建的進程中使用當(dāng)前的目錄和環(huán)境變量,則不需要任何配置,直接將命令行和參數(shù)傳入 ProcessBuilder 中,然后調(diào)用 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();
可以預(yù)先配置 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 的源代碼中找到對應(yīng)的本地方法,然后對其進行分析。JVM 對進程的實現(xiàn)相對比較簡單,以 Windows 下的 JVM 為例。在 JVM 中,將 Java 中調(diào)用方法時的傳入的參數(shù)傳遞給操作系統(tǒng)對應(yīng)的方法來實現(xiàn)相應(yīng)的功能。如表 1
表 1. JDK 中 native 方法與 Windows API 的對應(yīng)關(guān)系
以 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 中對應(yīng)的本地方法如代碼清單 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) { /* 設(shè)置內(nèi)部變量值 */ …… /* 建立輸入、輸出以及錯誤流管道 */ 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ù)格式的轉(zhuǎn)換 */ pcmd = (LPTSTR) JNU_GetStringPlatformChars(env, cmd, NULL); …… /* 調(diào)用系統(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)建一個進程的時候,調(diào)用 Windows 提供的 CreatePipe 方法建立輸入,輸出和錯誤管道,同時將用戶通過 Java 傳入的參數(shù)轉(zhuǎn)換為操作系統(tǒng)可以識別的 C 語言的格式,然后調(diào)用 Windows 提供的創(chuàng)建系統(tǒng)進程的方式,創(chuàng)建一個進程,同時在 JAVA 虛擬機中保存了這個進程對應(yīng)的句柄,然后返回給了 ProcessImpl 類,但是該類將返回句柄進行了隱藏。也正是 Java 跨平臺的特性體現(xiàn),JVM 盡可能的將和操作系統(tǒng)相關(guān)的實現(xiàn)細節(jié)進行了封裝,并隱藏了起來。
同樣,在用戶調(diào)用 close、waitfor、destory 以及 exitValue 方法以后, JVM 會首先取得之前保存的該進程在操作系統(tǒng)中的句柄,然后通過調(diào)用操作系統(tǒng)提供的接口對該進程進行操作。通過這種方式來實現(xiàn)對進程的操作。
在其它平臺下也是用類似的方式實現(xiàn)的,不同的是調(diào)用的對應(yīng)平臺的 API 會有所不同。
Java 進程與操作系統(tǒng)進程
通過上面對 Java 進程的分析,其實它在實現(xiàn)上就是創(chuàng)建了操作系統(tǒng)的一個進程,也就是每個 JVM 中創(chuàng)建的進程都對應(yīng)了操作系統(tǒng)中的一個進程。但是,Java 為了給用戶更好的更方便的使用,向用戶屏蔽了一些與平臺相關(guān)的信息,這為用戶需要使用的時候,帶來了些許不便。
在使用 C/C++ 創(chuàng)建系統(tǒng)進程的時候,是可以獲得進程的 PID 值的,可以直接通過該 PID 去操作相應(yīng)進程。但是在 JAVA 中,用戶只能通過實例的引用去進行操作,當(dāng)該引用丟失或者無法取得的時候,就無法了解任何該進程的信息。
當(dāng)然,Java 進程在使用的時候還有些要注意的事情:
1. Java 提供的輸入輸出的管道容量是十分有限的,如果不及時讀取會導(dǎo)致進程掛起甚至引起死鎖。
2. 當(dāng)創(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)進程相關(guān)的信息。同時,在使用 Java 提供創(chuàng)建進程運行本地命令的時候,需要小心使用。
一般而言,使用進程是為了執(zhí)行某項任務(wù),而現(xiàn)代操作系統(tǒng)對于執(zhí)行任務(wù)的計算資源的配置調(diào)度一般是以線程為對象(早期的類 Unix 系統(tǒng)因為不支持線程,所以進程也是調(diào)度單位,但那是比較輕量級的進程,在此不做深入討論)。創(chuàng)建一個進程,操作系統(tǒng)實際上還是會為此創(chuàng)建相應(yīng)的線程以運行一系列指令。特別地,當(dāng)一個任務(wù)比較龐大復(fù)雜,可能需要創(chuàng)建多個線程以實現(xiàn)邏輯上并發(fā)執(zhí)行的時候,線程的作用更為明顯。因而我們有必要深入了解 Java 中的線程,以避免可能出現(xiàn)的問題。本文下面的內(nèi)容即是呈現(xiàn) Java 線程的創(chuàng)建方式以及它與操作系統(tǒng)線程的聯(lián)系與區(qū)別。
Java 創(chuàng)建線程的方法
實際上,創(chuàng)建線程最重要的是提供線程函數(shù)(回調(diào)函數(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)建這個子類的對象,調(diào)用 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 對象中,同樣調(diào)用 Thread#start 方法就可以在一個新的線程中運行 run 方法中的代碼了。
Thread myThread = new Thread( new MyRunnable()); myThread.start();
可以看到,不管是用哪種方法,實際上都是要實現(xiàn)一個 run 方法的。 該方法本質(zhì)是上一個回調(diào)方法。由 start 方法新創(chuàng)建的線程會調(diào)用這個方法從而執(zhí)行需要的代碼。 從后面可以看到,run 方法并不是真正的線程函數(shù),只是被線程函數(shù)調(diào)用的一個 Java 方法而已,和其他的 Java 方法沒有什么本質(zhì)的不同。
Java 線程的實現(xiàn)
從概念上來說,一個 Java 線程的創(chuàng)建根本上就對應(yīng)了一個本地線程(native thread)的創(chuàng)建,兩者是一一對應(yīng)的。 問題是,本地線程執(zhí)行的應(yīng)該是本地代碼,而 Java 線程提供的線程函數(shù)是 Java 方法,編譯出的是 Java 字節(jié)碼,所以可以想象的是, Java 線程其實提供了一個統(tǒng)一的線程函數(shù),該線程函數(shù)通過 Java 虛擬機調(diào)用 Java 線程方法 , 這是通過 Java 本地方法調(diào)用來實現(xiàn)的。
以下是 Thread#start 方法的示例:
public synchronized void start() { … start0(); … }
可以看到它實際上調(diào)用了本地方法 start0, 該方法的聲明如下:
private native void start0();
Thread 類有個 registerNatives 本地方法,該方法主要的作用就是注冊一些本地方法供 Thread 類使用,如 start0(),stop0() 等等,可以說,所有操作本地線程的本地方法都是由它注冊的 . 這個方法放在一個 static 語句塊中,這就表明,當(dāng)該類被加載到 JVM 中的時候,它就會被調(diào)用,進而注冊相應(yīng)的本地方法。
private static native void registerNatives(); static{ registerNatives(); }
本地方法 registerNatives 是定義在 Thread.c 文件中的。Thread.c 是個很小的文件,定義了各個操作系統(tǒng)平臺都要用到的關(guān)于線程的公用數(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 線程調(diào)用 start 的方法,實際上會調(diào)用到 JVM_StartThread 方法,那這個方法又是怎樣的邏輯呢。實際上,我們需要的是(或者說 Java 表現(xiàn)行為)該方法最終要調(diào)用 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ù)內(nèi)創(chuàng)建了真正的平臺相關(guān)的本地線程,其線程函數(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); }
可以看到調(diào)用了 vmSymbolHandles::run_method_name 方法,這是在 vmSymbols.hpp 用宏定義的:
class vmSymbolHandles: AllStatic { … template(run_method_name,"run") … }
至于 run_method_name 是如何聲明定義的,因為涉及到很繁瑣的代碼細節(jié),本文不做贅述。感興趣的讀者可以自行查看 JVM 的源代碼。
圖 1. Java 線程創(chuàng)建調(diào)用關(guān)系圖
綜上所述,Java 線程的創(chuàng)建調(diào)用過程如 圖 1 所示,首先 , Java 線程的 start 方法會創(chuàng)建一個本地線程(通過調(diào)用 JVM_StartThread),該線程的線程函數(shù)是定義在 jvm.cpp 中的 thread_entry,由其再進一步調(diào)用 run 方法??梢钥吹?Java 線程的 run 方法和普通方法其實沒有本質(zhì)區(qū)別,直接調(diào)用 run 方法不會報錯,但是卻是在當(dāng)前線程執(zhí)行,而不會創(chuàng)建一個新的線程。
Java 線程與操作系統(tǒng)線程
從上我們知道,Java 線程是建立在系統(tǒng)本地線程之上的,是另一層封裝,其面向 Java 開發(fā)者提供的接口存在以下的局限性:
線程返回值
Java 沒有提供方法來獲取線程的退出返回值。實際上,線程可以有退出返回值,它一般被操作系統(tǒng)存儲在線程控制結(jié)構(gòu)中 (TCB),調(diào)用者可以通過檢測該值來確定線程是正常退出還是異常終止。
線程的同步
Java 提供方法 Thread#Join()來等待一個線程結(jié)束,一般情況這就足夠了,但一種可能的情況是,需要等待在多個線程上(比如任意一個線程結(jié)束或者所有線程結(jié)束才會返回),循環(huán)調(diào)用每個線程的 Join 方法是不可行的,這可能導(dǎo)致很奇怪的同步問題。
線程的 ID
Java 提供的方法 Thread#getID()返回的是一個簡單的計數(shù) ID,其實和操作系統(tǒng)線程的 ID 沒有任何關(guān)系。
線程運行時間統(tǒng)計,Java 沒有提供方法來獲取線程中某段代碼的運行時間的統(tǒng)計結(jié)果。雖然可以自行使用計時的方法來實現(xiàn)(獲取運行開始和結(jié)束的時間,然后相減 ),但由于存在多線程調(diào)度方法的原因,無法獲取線程實際使用的 CPU 運算時間,因而必然是不準確的。
總結(jié)
本文通過對 Java 進程和線程的分析,可以看出 Java 對這兩種操作系統(tǒng) “資源” 進行了封裝,使得開發(fā)人員只需關(guān)注如何使用這兩種 “資源” ,而不必過多的關(guān)心細節(jié)。這樣的封裝一方面降低了開發(fā)人員的工作復(fù)雜度,提高了工作效率;另一方面由于封裝屏蔽了操作系統(tǒng)本身的一些特性,因而在使用 Java 進程線程時有了某些限制,這是封裝不可避免的問題。語言的演化本就是決定需要什么不需要什么的過程,相信隨著 Java 的不斷發(fā)展,封裝的功能子集的必然越來越完善。
- java中進程與線程_三種實現(xiàn)方式總結(jié)(必看篇)
- java多線程之線程,進程和Synchronized概念初解
- Java多線程并發(fā)執(zhí)行demo代碼實例
- java高并發(fā)之理解進程和線程
- Java多線程高并發(fā)中解決ArrayList與HashSet和HashMap不安全的方案
- java多線程之并發(fā)工具類CountDownLatch,CyclicBarrier和Semaphore
- Java的線程與進程以及線程的四種創(chuàng)建方式
- Java并發(fā)編程之volatile與JMM多線程內(nèi)存模型
- Java多線程并發(fā)與并行和線程與進程案例
相關(guān)文章
Hibernate映射解析之關(guān)聯(lián)映射詳解
所謂關(guān)聯(lián)映射就是將關(guān)聯(lián)關(guān)系映射到數(shù)據(jù)庫里,在對象模型中就是一個或多個引用。下面這篇文章詳細的給大家介紹了Hibernate映射解析之關(guān)聯(lián)映射的相關(guān)資料,需要的朋友可以參考借鑒,下面來一起看看吧。2017-02-02java?常規(guī)輪詢長輪詢Long?polling實現(xiàn)示例詳解
這篇文章主要為大家介紹了java?常規(guī)輪詢長輪詢Long?polling實現(xiàn)示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2022-12-12Spring AOP的幾種實現(xiàn)方式總結(jié)
本篇文章主要介紹了Spring AOP的幾種實現(xiàn)方式總結(jié),小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2017-02-02Springboot解決跨域問題方案總結(jié)(包括Nginx,Gateway網(wǎng)關(guān)等)
跨域問題是瀏覽器為了保護用戶的信息安全,實施了同源策略(Same-Origin?Policy),即只允許頁面請求同源(相同協(xié)議、域名和端口)的資源,本文給大家總結(jié)了Springboot解決跨域問題方案包括Nginx,Gateway網(wǎng)關(guān)等),需要的朋友可以參考下2024-03-03