程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> JAVA編程 >> 關於JAVA >> Java中的進程與線程的實現

Java中的進程與線程的實現

編輯:關於JAVA

概述

進程與線程,本質意義上說, 是操作系統的調度單位,可以看成是一種操作系統 “資源” 。Java 作為與 平台無關的編程語言,必然會對底層(操作系統)提供的功能進行進一步的封裝,以平台無關的編程接口供程序員使用,進 程與線程作為操作系統核心概念的一部分無疑亦是如此。在 Java 語言中,對進程和線程的封裝,分別提供了 Process 和 Thread 相關的一些類。本文首先簡單的介紹如何使用這些類來創建進程和線程,然後著重介紹這些類是如何和操作系統本 地進程線程相對應的,給出了 Java 虛擬機對於這些封裝類的概要性的實現;同時由於 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 引用。

Runtime.exec 方法建立一個本地進程

該方法在 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 方法來建立一個本地的進程

如果希望在新創建的進程中使用當前的目錄和環境變量 ,則不需要任何配置,直接將命令行和參數傳入 ProcessBuilder 中,然後調用 start 方法,就可以獲得進程的引用。

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 方法創建 的進程,對修改之前創建的進程實例沒有影響。

JVM 對進程的實現

在 JDK 的代碼中,只提供了 ProcessImpl 類來實現 Process 抽象類。其中引用了 native 的 create, close, waitfor, destory 和 exitValue 方法 。在 Java 中,native 方法是依賴於操作系統平台的本地方法,它的實現是用 C/C++ 等類似的底層語言實現。我們可以在 JVM 的源代碼中找到對應的本地方法,然後對其進行分析。JVM 對進程的實現相對比較簡單,以 Windows 下的 JVM 為例。 在 JVM 中,將 Java 中調用方法時的傳入的參數傳遞給操作系統對應的方法來實現相應的功能。如表 1

JDK 中調用的 native 方法名 對應調用的 Windows API create CreateProcess,CreatePipe close CloseHandle waitfor WaitForMultipleObjects destroy TerminateProcess exitValue GetExitCodeProcess

以 create 方法為例,我們看一下它是如何和系統 API 進行連接的 。

在 ProcessImple 類中,存在 native 的 create 方法,其參數如下:

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; 
        } 
    /* 進行參數格式的轉換 */ 
        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 進程與操作系統進程

通過上面對 Java 進程的分析,其實它在實現上就是創建了操作系統的一個進程,也就是每個 JVM 中創建的進程都 對應了操作系統中的一個進程。但是,Java 為了給用戶更好的更方便的使用,向用戶屏蔽了一些與平台相關的信息,這為 用戶需要使用的時候,帶來了些許不便。

在使用 C/C++ 創建系統進程的時候,是可以獲得進程的 PID 值的,可以 直接通過該 PID 去操作相應進程。但是在 JAVA 中,用戶只能通過實例的引用去進行操作,當該引用丟失或者無法取得的 時候,就無法了解任何該進程的信息。

當然,Java 進程在使用的時候還有些要注意的事情:

Java 提供的 輸入輸出的管道容量是十分有限的,如果不及時讀取會導致進程掛起甚至引起死鎖。

當創建進程去執行 Windows 下 的系統命令時,如:dir、copy 等。需要運行 windows 的命令解釋器,command.exe/cmd.exe,這依賴於 windows 的版本 ,這樣才可以運行系統的命令。

對於 Shell 中的管道 ‘  |  ’命令,各平台下的重定向命令符 ‘ > ’,都無法通過命令參數直接傳入進行實現,而需要在 Java 代碼中做一些處理,如定義新的流來存儲標准輸出,等 等問題。

總之,Java 中對操作系統的進程進行了封裝,屏蔽了操作系統進程相關的信息。同時,在使用 Java 提供 創建進程運行本地命令的時候,需要小心使用。

一般而言,使用進程是為了執行某項任務,而現代操作系統對於執 行任務的計算資源的配置調度一般是以線程為對象(早期的類 Unix 系統因為不支持線程,所以進程也是調度單位,但那是 比較輕量級的進程,在此不做深入討論)。創建一個進程,操作系統實際上還是會為此創建相應的線程以運行一系列指令。 特別地,當一個任務比較龐大復雜,可能需要創建多個線程以實現邏輯上並發執行的時候,線程的作用更為明顯。因而我們 有必要深入了解 Java 中的線程,以避免可能出現的問題。本文下面的內容即是呈現 Java 線程的創建方式以及它與操作系 統線程的聯系與區別。

Java 創建線程的方法

實際上,創建線程最重要的是提供線程函數(回調函數),該 函數作為新創建線程的入口函數,實現自己想要的功能。Java 提供了兩種方法來創建一個線程:

繼承 Thread 類

class MyThread extends Thread{
 public void run() {  
  System.out.println("My thread is started.");
 }
}

實現該繼承類的 run 方法,然後就可以創建這個子類的對象,調用 start 方法即可創建一個新的線程:

MyThread myThread = new MyThread();
 myThread.start();

實現 Runnable 接口

class MyRunnable implements Runnable{
 public void run() {
     System.out.println("My runnable is invoked.");
 }
}

實現 Runnable 接口的類的對象可以作為一個參數傳遞到創建的 Thread 對象中,同樣調用 Thread#start 方法就可以在一個新的線程中運 行 run 方法中的代碼了。

Thread myThread = new Thread( new MyRunnable());
 myThread.start ();

可以看到,不管是用哪種方法,實際上都是要實現一個 run 方法的。 該方法本質是上一個回調方法。由 start 方法新創建的線程會調用這個方法從而執行需要的代碼。 從後面可以看到,run 方法並不是真正的線程函數,只是 被線程函數調用的一個 Java 方法而已,和其他的 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 所示。

清單 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 所示。

清單 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 沒有提供方法來獲取 線程的退出返回值。實際上,線程可以有退出返回值,它一般被操作系統存儲在線程控制結構中 (TCB),調用者可以通過 檢測該值來確定線程是正常退出還是異常終止。

線程的同步

Java 提供方法 Thread#Join()來等待一個線 程結束,一般情況這就足夠了,但一種可能的情況是,需要等待在多個線程上(比如任意一個線程結束或者所有線程結束才 會返回),循環調用每個線程的 Join 方法是不可行的,這可能導致很奇怪的同步問題。

線程的 ID

Java 提 供的方法 Thread#getID()返回的是一個簡單的計數 ID,其實和操作系統線程的 ID 沒有任何關系。

線程運行時 間統計

Java 沒有提供方法來獲取線程中某段代碼的運行時間的統計結果。雖然可以自行使用計時的方法來實現(獲 取運行開始和結束的時間,然後相減 ),但由於存在多線程調度方法的原因,無法獲取線程實際使用的 CPU 運算時間,因 而必然是不准確的。

總結

本文通過對 Java 進程和線程的分析,可以看出 Java 對這兩種操作系統 “資源” 進行了封裝,使得開發人員只需關 注如何使用這兩種 “資源” ,而不必過多的關心細節。這樣的封裝一方面降低了開發人員的工作復雜度,提高了工作效率 ;另一方面由於封裝屏蔽了操作系統本身的一些特性,因而在使用 Java 進程線程時有了某些限制,這是封裝不可避免的問 題。語言的演化本就是決定需要什麼不需要什麼的過程,相信隨著 Java 的不斷發展,封裝的功能子集的必然越來越完善。

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved