一般我們在java中運行其它類中的方法時,無論是靜態調用,還是動態調用,都是在當前的進程中執行的,也就是說,只有一個java虛擬機實例在運行。而有的時候,我們需要通過java代碼啟動多個java子進程。這樣做雖然占用了一些系統資源,但會使程序更加穩定,因為新啟動的程序是在不同的虛擬機進程中運行的,如果有一個進程發生異常,并不影響其它的子進程。
在Java中我們可以使用兩種方法來實現這種要求。最簡單的方法就是通過Runtime中的exec方法執行java classname。如果執行成功,這個方法返回一個Process對象,如果執行失敗,將拋出一個IOException錯誤。下面讓我們來看一個簡單的例子。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
// Test1.java文件 import java.io.*; public class Test { public static void main(String[] args) { FileOutputStream fOut = new FileOutputStream( "c://Test1.txt" ); fOut.close(); System.out.println( "被調用成功!" ); } } // Test_Exec.java public class Test_Exec { public static void main(String[] args) { Runtime run = Runtime.getRuntime(); Process p = run.exec( "java test1" ); } } |
通過java Test_Exec運行程序后,發現在C盤多了個Test1.txt文件,但在控制臺中并未出現"被調用成功!"的輸出信息。因此可以斷定,Test已經被執行成功,但因為某種原因,Test的輸出信息未在Test_Exec的控制臺中輸出。這個原因也很簡單,因為使用exec建立的是Test_Exec 的子進程,這個子進程并沒有自己的控制臺,因此,它并不會輸出任何信息。
如果要輸出子進程的輸出信息,可以通過Process中的getInputStream得到子進程的輸出流(在子進程中輸出,在父進程中就是輸入),然后將子進程中的輸出流從父進程的控制臺輸出。具體的實現代碼如下如示:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
// Test_Exec_Out.java import java.io.*; public class Test_Exec_Out { public static void main(String[] args) { Runtime run = Runtime.getRuntime(); Process p = run.exec( "java test1" ); BufferedInputStream in = new BufferedInputStream(p.getInputStream()); BufferedReader br = new BufferedReader( new InputStreamReader(in)); String s; while ((s = br.readLine()) != null ) System.out.println(s); } } |
從上面的代碼可以看出,在Test_Exec_Out.java中通過按行讀取子進程的輸出信息,然后在Test_Exec_Out中按每行進行輸出。上面討論的是如何得到子進程的輸出信息。那么,除了輸出信息,還有輸入信息。既然子進程沒有自己的控制臺,那么輸入信息也得由父進程提供。我們可以通過 Process的getOutputStream方法來為子進程提供輸入信息(即由父進程向子進程輸入信息,而不是由控制臺輸入信息)。我們可以看看如下的代碼:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
|
// Test2.java文件 import java.io.*; public class Test { public static void main(String[] args) { BufferedReader br = new BufferedReader( new InputStreamReader(System.in)); System.out.println( "由父進程輸入的信息:" + br.readLine()); } } // Test_Exec_In.java import java.io.*; public class Test_Exec_In { public static void main(String[] args) { Runtime run = Runtime.getRuntime(); Process p = run.exec( "java test2" ); BufferedWriter bw = new BufferedWriter( new OutputStreamWriter(p.getOutputStream())); bw.write( "向子進程輸出信息" ); bw.flush(); bw.close(); // 必須得關閉流,否則無法向子進程中輸入信息 // System.in.read(); } } |
從以上代碼可以看出,Test1得到由Test_Exec_In發過來的信息,并將其輸出。當你不加bw.flash()和bw.close()時,信息將無法到達子進程,也就是說子進程進入阻塞狀態,但由于父進程已經退出了,因此,子進程也跟著退出了。如果要證明這一點,可以在最后加上 System.in.read(),然后通過任務管理器(在windows下)查看java進程,你會發現如果加上bw.flush()和 bw.close(),只有一個java進程存在,如果去掉它們,就有兩個java進程存在。這是因為,如果將信息傳給Test2,在得到信息后, Test2就退出了。在這里有一點需要說明一下,exec的執行是異步的,并不會因為執行的某個程序阻塞而停止執行下面的代碼。因此,可以在運行 test2后,仍可以執行下面的代碼。
exec方法經過了多次的重載。上面使用的只是它的一種重載。它還可以將命令和參數分開,如exec("java.test2")可以寫成exec("java", "test2")。exec還可以通過指定的環境變量運行不同配置的java虛擬機。
除了使用Runtime的exec方法建立子進程外,還可以通過ProcessBuilder建立子進程。ProcessBuilder的使用方法如下:
1
2
3
4
5
6
7
8
9
10
11
|
// Test_Exec_Out.java import java.io.*; public class Test_Exec_Out { public static void main(String[] args) { ProcessBuilder pb = new ProcessBuilder( "java" , "test1" ); Process p = pb.start(); … … } } |
在建立子進程上,ProcessBuilder和Runtime類似,不同的ProcessBuilder使用start()方法啟動子進程,而Runtime使用exec方法啟動子進程。得到Process后,它們的操作就完全一樣的。
ProcessBuilder和Runtime一樣,也可設置可執行文件的環境信息、工作目錄等。下面的例子描述了如何使用ProcessBuilder設置這些信息。
1
2
3
4
5
6
7
8
|
ProcessBuilder pb = new ProcessBuilder( "Command" , "arg2" , "arg2" , '' '); // 設置環境變量 Map<String, String> env = pb.environment(); env.put( "key1" , "value1" ); env.remove( "key2" ); env.put( "key2" , env.get( "key1" ) + "_test" ); pb.directory( "../abcd" ); // 設置工作目錄 Process p = pb.start(); // 建立子進程 |
進程阻塞問題
由Process代表的進程在某些平臺上有時候并不能很好的工作,特別是在對代表進程的標準輸入流、輸出流和錯誤輸出進行操作時,如果使用不慎,有可能導致進程阻塞,甚至死鎖。
如果將以上事例中的從標準輸出重讀取信息的語句修改為從錯誤輸出流中讀?。?nbsp;
1
|
stdout = new BufferedReader( new InputStreamReader(p.getErrorStream())); |
那么程序將發生阻塞,不能執行完成,而是hang在那里。
當進程啟動后,就會打開標準輸出流和錯誤輸出流準備輸出,當進程結束時,就會關閉他們。在以上例子中,錯誤輸出流沒有數據要輸出,標準輸出流中有數據輸出。由于標準輸出流中的數據沒有被讀取,進程就不會結束,錯誤輸出流也就不會被關閉,因此在調用readLine()方法時,整個程序就會被阻塞。為了解決這個問題,可以根據輸出的實際先后,先讀取標準輸出流,然后讀取錯誤輸出流。
但是,很多時候不能很明確的知道輸出的先后,特別是要操作標準輸入的時候,情況就會更為復雜。這時候可以采用線程來對標準輸出、錯誤輸出和標準輸入進行分別處理,根據他們之間在業務邏輯上的關系決定讀取那個流或者寫入數據。
針對標準輸出流和錯誤輸出流所造成的問題,可以使用ProcessBuilder的redirectErrorStream()方法將他們合二為一,這時候只要讀取標準輸出的數據就可以了。
當在程序中使用Process的waitFor()方法時,特別是在讀取之前調用waitFor()方法時,也有可能造成阻塞??梢杂镁€程的方法來解決這個問題,也可以在讀取數據后,調用waitFor()方法等待程序結束。
總之,解決阻塞的方法在這里我介紹使用ProcessBuilder類,利用redirectErrorStream方法將標準輸出流和錯誤輸出流合二為一,在用start()方法啟動進程后,先從標準輸出中讀取數據,然后調用waitFor()方法等待進程結束。
如:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
|
import java.io.BufferedReader; import java.io.File; import java.io.InputStreamReader; import java.util.ArrayList; import java.util.List; public class Test3 { public static void main(String[] args) { try { List<String> list = new ArrayList<String>(); ProcessBuilder pb = null ; Process p = null ; String line = null ; BufferedReader stdout = null ; //list the files and directorys under C:\ list.add( "CMD.EXE" ); list.add( "/C" ); list.add( "dir1" ); pb = new ProcessBuilder(list); pb.directory( new File( "C:\\" )); //merge the error output with the standard output pb.redirectErrorStream( true ); p = pb.start(); //read the standard output stdout = new BufferedReader( new InputStreamReader(p .getInputStream())); while ((line = stdout.readLine()) != null ) { System.out.println(line); } int ret = p.waitFor(); System.out.println( "the return code is " + ret); stdout.close(); } catch (Exception e) { e.printStackTrace(); } } |