详解Java中多进程编程的实现
1.Java进程的创建
Java提供了两种方法用来启动进程或其它程序:
(1)使用Runtime的exec()方法
(2)使用ProcessBuilder的start()方法
1.1ProcessBuilder
ProcessBuilder类是J2SE1.5在java.lang中新添加的一个新类,此类用于创建操作系统进程,它提供一种启动和管理进程(也就是应用程序)的方法。在J2SE1.5之前,都是由Process类处来实现进程的控制管理。
每个ProcessBuilder实例管理一个进程属性集。start()方法利用这些属性创建一个新的Process实例。start()方法可以从同一实例重复调用,以利用相同的或相关的属性创建新的子进程。
每个进程生成器管理这些进程属性:
命令是一个字符串列表,它表示要调用的外部程序文件及其参数(如果有)。在此,表示有效的操作系统命令的字符串列表是依赖于系统的。例如,每一个总体变量,通常都要成为此列表中的元素,但有一些操作系统,希望程序能自己标记命令行字符串——在这种系统中,Java实现可能需要命令确切地包含这两个元素。
环境是从变量到值的依赖于系统的映射。初始值是当前进程环境的一个副本(请参阅System.getenv())。
工作目录。默认值是当前进程的当前工作目录,通常根据系统属性user.dir来命名。
redirectErrorStream属性。最初,此属性为false,意思是子进程的标准输出和错误输出被发送给两个独立的流,这些流可以通过Process.getInputStream()和Process.getErrorStream()方法来访问。如果将值设置为true,标准错误将与标准输出合并。这使得关联错误消息和相应的输出变得更容易。在此情况下,合并的数据可从Process.getInputStream()返回的流读取,而从Process.getErrorStream()返回的流读取将直接到达文件尾。
修改进程构建器的属性将影响后续由该对象的start()方法启动的进程,但从不会影响以前启动的进程或Java自身的进程。大多数错误检查由start()方法执行。可以修改对象的状态,但这样start()将会失败。例如,将命令属性设置为一个空列表将不会抛出异常,除非包含了start()。
注意,此类不是同步的。如果多个线程同时访问一个ProcessBuilder,而其中至少一个线程从结构上修改了其中一个属性,它必须保持外部同步。
构造方法摘要
ProcessBuilder(List<String>command)
利用指定的操作系统程序和参数构造一个进程生成器。
ProcessBuilder(String...command)
利用指定的操作系统程序和参数构造一个进程生成器。
方法摘要
List<String>command()
返回此进程生成器的操作系统程序和参数。
ProcessBuildercommand(List<String>command)
设置此进程生成器的操作系统程序和参数。
ProcessBuildercommand(String...command)
设置此进程生成器的操作系统程序和参数。
Filedirectory()
返回此进程生成器的工作目录。
ProcessBuilderdirectory(Filedirectory)
设置此进程生成器的工作目录。
Map<String,String>environment()
返回此进程生成器环境的字符串映射视图。
booleanredirectErrorStream()
通知进程生成器是否合并标准错误和标准输出。
ProcessBuilderredirectErrorStream(booleanredirectErrorStream)
设置此进程生成器的redirectErrorStream属性。
Processstart()
使用此进程生成器的属性启动一个新进程。
1.2Runtime
每个Java应用程序都有一个Runtime类实例,使应用程序能够与其运行的环境相连接。可以通过getRuntime方法获取当前运行时。
应用程序不能创建自己的Runtime类实例。但可以通过getRuntime方法获取当前Runtime运行时对象的引用。一旦得到了一个当前的Runtime对象的引用,就可以调用Runtime对象的方法去控制Java虚拟机的状态和行为。
Java代码 收藏代码
voidaddShutdownHook(Threadhook)
注册新的虚拟机来关闭挂钩。
intavailableProcessors()
向Java虚拟机返回可用处理器的数目。
Processexec(Stringcommand)
在单独的进程中执行指定的字符串命令。
Processexec(String[]cmdarray)
在单独的进程中执行指定命令和变量。
Processexec(String[]cmdarray,String[]envp)
在指定环境的独立进程中执行指定命令和变量。
Processexec(String[]cmdarray,String[]envp,Filedir)
在指定环境和工作目录的独立进程中执行指定的命令和变量。
Processexec(Stringcommand,String[]envp)
在指定环境的单独进程中执行指定的字符串命令。
Processexec(Stringcommand,String[]envp,Filedir)
在有指定环境和工作目录的独立进程中执行指定的字符串命令。
voidexit(intstatus)
通过启动虚拟机的关闭序列,终止当前正在运行的Java虚拟机。
longfreeMemory()
返回Java虚拟机中的空闲内存量。
voidgc()
运行垃圾回收器。
InputStreamgetLocalizedInputStream(InputStreamin)
已过时。从JDK1.1开始,将本地编码字节流转换为Unicode字符流的首选方法是使用InputStreamReader和BufferedReader类。
OutputStreamgetLocalizedOutputStream(OutputStreamout)
已过时。从JDK1.1开始,将Unicode字符流转换为本地编码字节流的首选方法是使用OutputStreamWriter、BufferedWriter和PrintWriter类。
staticRuntimegetRuntime()
返回与当前Java应用程序相关的运行时对象。
voidhalt(intstatus)
强行终止目前正在运行的Java虚拟机。
voidload(Stringfilename)
加载作为动态库的指定文件名。
voidloadLibrary(Stringlibname)
加载具有指定库名的动态库。
longmaxMemory()
返回Java虚拟机试图使用的最大内存量。
booleanremoveShutdownHook(Threadhook)
取消注册某个先前已注册的虚拟机关闭挂钩。
voidrunFinalization()
运行挂起finalization的所有对象的终止方法。
staticvoidrunFinalizersOnExit(booleanvalue)
已过时。此方法本身具有不安全性。它可能对正在使用的对象调用终结方法,而其他线程正在操作这些对象,从而导致不正确的行为或死锁。
longtotalMemory()
返回Java虚拟机中的内存总量。
voidtraceInstructions(booleanon)
启用/禁用指令跟踪。
voidtraceMethodCalls(booleanon)
启用/禁用方法调用跟踪。
1.3Process
不管通过那种方法启动进程后,都会返回一个Process类的实例代表启动的进程,该实例可用来控制进程并获得相关信息。Process类提供了执行从进程输入、执行输出到进程、等待进程完成、检查进程的退出状态以及销毁(杀掉)进程的方法:
voiddestroy()
杀掉子进程。
一般情况下,该方法并不能杀掉已经启动的进程,不用为好。
intexitValue()
返回子进程的出口值。
只有启动的进程执行完成、或者由于异常退出后,exitValue()方法才会有正常的返回值,否则抛出异常。
InputStreamgetErrorStream()
获取子进程的错误流。
如果错误输出被重定向,则不能从该流中读取错误输出。
InputStreamgetInputStream()
获取子进程的输入流。
可以从该流中读取进程的标准输出。
OutputStreamgetOutputStream()
获取子进程的输出流。
写入到该流中的数据作为进程的标准输入。
intwaitFor()
导致当前线程等待,如有必要,一直要等到由该Process对象表示的进程已经终止。
2.多进程编程实例
一般我们在java中运行其它类中的方法时,无论是静态调用,还是动态调用,都是在当前的进程中执行的,也就是说,只有一个java虚拟机实例在运行。而有的时候,我们需要通过java代码启动多个java子进程。这样做虽然占用了一些系统资源,但会使程序更加稳定,因为新启动的程序是在不同的虚拟机进程中运行的,如果有一个进程发生异常,并不影响其它的子进程。
在Java中我们可以使用两种方法来实现这种要求。最简单的方法就是通过Runtime中的exec方法执行javaclassname。如果执行成功,这个方法返回一个Process对象,如果执行失败,将抛出一个IOException错误。下面让我们来看一个简单的例子。
//Test1.java文件 importjava.io.*; publicclassTest { publicstaticvoidmain(String[]args) { FileOutputStreamfOut=newFileOutputStream("c:\\Test1.txt"); fOut.close(); System.out.println("被调用成功!"); } } //Test_Exec.java publicclassTest_Exec { publicstaticvoidmain(String[]args) { Runtimerun=Runtime.getRuntime(); Processp=run.exec("javatest1"); } }
通过javaTest_Exec运行程序后,发现在C盘多了个Test1.txt文件,但在控制台中并未出现"被调用成功!"的输出信息。因此可以断定,Test已经被执行成功,但因为某种原因,Test的输出信息未在Test_Exec的控制台中输出。这个原因也很简单,因为使用exec建立的是Test_Exec的子进程,这个子进程并没有自己的控制台,因此,它并不会输出任何信息。
如果要输出子进程的输出信息,可以通过Process中的getInputStream得到子进程的输出流(在子进程中输出,在父进程中就是输入),然后将子进程中的输出流从父进程的控制台输出。具体的实现代码如下如示:
//Test_Exec_Out.java importjava.io.*; publicclassTest_Exec_Out { publicstaticvoidmain(String[]args) { Runtimerun=Runtime.getRuntime(); Processp=run.exec("javatest1"); BufferedInputStreamin=newBufferedInputStream(p.getInputStream()); BufferedReaderbr=newBufferedReader(newInputStreamReader(in)); Strings; while((s=br.readLine())!=null) System.out.println(s); } }
从上面的代码可以看出,在Test_Exec_Out.java中通过按行读取子进程的输出信息,然后在Test_Exec_Out中按每行进行输出。上面讨论的是如何得到子进程的输出信息。那么,除了输出信息,还有输入信息。既然子进程没有自己的控制台,那么输入信息也得由父进程提供。我们可以通过Process的getOutputStream方法来为子进程提供输入信息(即由父进程向子进程输入信息,而不是由控制台输入信息)。我们可以看看如下的代码:
//Test2.java文件 importjava.io.*; publicclassTest { publicstaticvoidmain(String[]args) { BufferedReaderbr=newBufferedReader(newInputStreamReader(System.in)); System.out.println("由父进程输入的信息:"+br.readLine()); } } //Test_Exec_In.java importjava.io.*; publicclassTest_Exec_In { publicstaticvoidmain(String[]args) { Runtimerun=Runtime.getRuntime(); Processp=run.exec("javatest2"); BufferedWriterbw=newBufferedWriter(newOutputStreamWriter(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的使用方法如下:
//Test_Exec_Out.java importjava.io.*; publicclassTest_Exec_Out { publicstaticvoidmain(String[]args) { ProcessBuilderpb=newProcessBuilder("java","test1"); Processp=pb.start(); …… } }
在建立子进程上,ProcessBuilder和Runtime类似,不同的ProcessBuilder使用start()方法启动子进程,而Runtime使用exec方法启动子进程。得到Process后,它们的操作就完全一样的。
ProcessBuilder和Runtime一样,也可设置可执行文件的环境信息、工作目录等。下面的例子描述了如何使用ProcessBuilder设置这些信息。
ProcessBuilderpb=newProcessBuilder("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");//设置工作目录 Processp=pb.start();//建立子进程