java多进程实现
原文链接: https://blog.youkuaiyun.com/weixin_28902749/article/details/114032221
看了下网上大多对多线程实现多一些,前阵子遇到了多进程,就记录一下,顺便自己也加深一下理解。
1、java创建进程以及启动
java为进程的创建以及启动提供了两种方式。
- 使用Runtime的exec()方法启动进程
- 使用ProcessBuilder的start()方法启动进程
1.1、ProcessBuilder
ProcessBuilder类是J2SE 1.5在java.lang中新添加的一个新类,此类用于创建操作系统进程,它提供一种启动和管理进程(也就是应用程序)的方法。在J2SE 1.5之前,都是由Process类处来实现进程的控制管理。
每个 ProcessBuilder 实例管理一个进程属性集。start() 方法利用这些属性创建一个新的 Process 实例。start() 方法可以从同一实例重复调用,以利用相同的或相关的属性创建新的子进程。
每个进程生成器管理这些进程属性:
-
命令
- 是一个字符串列表,它表示要调用的外部程序文件及其参数(如果有)。在此,表示有效的操作系统命令的字符串列表是依赖于系统的。
-
环境
- 是从变量 到值 的依赖于系统的映射。初始值是当前进程环境的一个副本(请参阅 System.getenv())。
-
工作目录
- 默认值是当前进程的当前工作目录,通常根据系统属性 user.dir 来命名。
-
redirectErrorStream 属性
-
最初,此属性为 false,意思是子进程的标准输出和错误输出被发送给两个独立的流,这些流可以通过 Process.getInputStream() 和 Process.getErrorStream() 方法来访问。如果将值设置为 true,标准错误将与标准输出合并。这使得关联错误消息和相应的输出变得更容易。在此情况下,合并的数据可从 Process.getInputStream() 返回的流读取,而从 Process.getErrorStream() 返回的流读取将直接到达文件尾。
注意: 修改进程构建器的属性将影响后续由该对象的 start() 方法启动的进程,但从不会影响以前启动的进程或 Java 自身的进程。大多数错误检查由 start() 方法执行。可以修改对象的状态,但这样 start() 将会失败。例如,将命令属性设置为一个空列表将不会抛出异常,除非包含了 start()。
注意: 此类不是同步的。如果多个线程同时访问一个 ProcessBuilder,而其中至少一个线程从结构上修改了其中一个属性,它必须 保持外部同步。
-
构造子进程方法摘要:
-
ProcessBuilder(List command)
- 利用指定的操作系统程序和参数构造一个进程生成器。
-
ProcessBuilder(String command)
- 利用指定的操作系统程序和参数构造一个进程生成器。
ProcessBuilder类方法摘要:
-
List command()
- 返回此进程生成器的操作系统程序和参数。
-
ProcessBuilder command(List command)
- 设置此进程生成器的操作系统程序和参数。
-
ProcessBuilder command(String… command)
- 设置此进程生成器的操作系统程序和参数。
-
File directory()
- 返回此进程生成器的工作目录。
-
ProcessBuilder directory(File directory)
- 设置此进程生成器的工作目录。
-
Map environment()
- 返回此进程生成器环境的字符串映射视图。
-
boolean redirectErrorStream()
- 通知进程生成器是否合并标准错误和标准输出。
-
ProcessBuilder redirectErrorStream(boolean redirectErrorStream)
- 设置此进程生成器的 redirectErrorStream 属性。
-
Process start()
- 使用此进程生成器的属性启动一个新进程。
示例代码:
1.2、 Runtime
每个 Java 应用程序都有一个 Runtime 类实例,使应用程序能够与其运行的环境相连接。可以通过 getRuntime() 方法获取当前运行时,如下操作。
Runtime run = Runtime.getRuntime();
**注意:**应用程序不能创建自己的 Runtime 类实例。但可以通过 getRuntime () 方法获取当前Runtime运行时对象的引用。一旦得到了一个当前的Runtime对象的引用,就可以调用Runtime对象的方法去控制Java虚拟机的状态和行为。
Runtime对象引用方法摘要:
- void addShutdownHook(Thread hook)
- 注册新的虚拟机来关闭挂钩。
- int availableProcessors()
- 向 Java 虚拟机返回可用处理器的数目。
- Process exec(String command)
- 在单独的进程中执行指定的字符串命令。
- Process exec(String[] cmdarray)
- 在单独的进程中执行指定命令和变量。
- Process exec(String[] cmdarray, String[] envp)
- 在指定环境的独立进程中执行指定命令和变量。
- Process exec(String[] cmdarray, String[] envp, File dir)
- 在指定环境和工作目录的独立进程中执行指定的命令和变量。
- Process exec(String command, String[] envp)
- 在指定环境的单独进程中执行指定的字符串命令。
- Process exec(String command, String[] envp, File dir)
- 在有指定环境和工作目录的独立进程中执行指定的字符串命令。
- void exit(int status)
- 通过启动虚拟机的关闭序列,终止当前正在运行的 Java 虚拟机。
- long freeMemory()
- 返回 Java 虚拟机中的空闲内存量。
- void gc()
- 运行垃圾回收器。
- static Runtime getRuntime()
- 返回与当前 Java 应用程序相关的运行时对象。
- void halt(int status)
- 强行终止目前正在运行的 Java 虚拟机。
- void load(String filename)
- 加载作为动态库的指定文件名。
- void loadLibrary(String libname)
- 加载具有指定库名的动态库。
- long maxMemory()
- 返回 Java 虚拟机试图使用的最大内存量。
- boolean removeShutdownHook(Thread hook)
- 取消注册某个先前已注册的虚拟机关闭挂钩。
- void runFinalization()
- 运行挂起 finalization 的所有对象的终止方法。
- static void runFinalizersOnExit(boolean value)
- 已过时。 此方法本身具有不安全性。它可能对正在使用的对象调用终结方法,而其他线程正在操作这些对象,从而导致不正确的行为或死锁。
- long totalMemory()
- 返回 Java 虚拟机中的内存总量。
- void traceInstructions(boolean on)
- 启用/禁用指令跟踪。
- void traceMethodCalls(boolean on)
- 启用/禁用方法调用跟踪。
1.3、 Process
不管通过那种方法启动进程后,都会返回一个Process类的实例代表启动的进程,该实例可用来控制进程并获得相关信息。Process 类提供了执行从进程输入、执行输出到进程、等待进程完成、检查进程的退出状态以及销毁(杀掉)进程的方法:
- void destroy()
- 杀掉子进程。一般情况下,该方法并不能杀掉已经启动的进程,不用为好。
- int exitValue()
- 返回子进程的出口值。只有启动的进程执行完成、或者由于异常退出后,exitValue()方法才会有正常的返回值,否则抛出异常。
- InputStream getErrorStream()
- 获取子进程的错误流。如果错误输出被重定向,则不能从该流中读取错误输出。
- InputStream getInputStream()
- 获取子进程的输入流。可以从该流中读取进程的标准输出。
- OutputStream getOutputStream()
- 获取子进程的输出流。写入到该流中的数据作为进程的标准输入。
- int waitFor()
- 导致当前线程等待,如有必要,一直要等到由该 Process 对象表示的进程已经终止。
2、多进程编程实例
多进程编程实例一般我们在java中运行其它类中的方法时,无论是静态调用,还是动态调用,都是在当前的进程中执行的,也就是说,只有一个java虚拟机实例在运行。而有的时候,我们需要通过java代码启动多个java子进程。这样做虽然占用了一些系统资源,但会使程序更加稳定,因为新启动的程序是在不同的虚拟机进程中运行的,如果有一个子进程发生异常,并不影响其它的子进程。
2.1、ProcessBuilder 示例
如果执行成功,这个方法返回一个Process对象,如果执行失败,将抛出一个IOException错误。
TestProcess.java文件
import java.io.*;
public class TestProcess {
public static void main(String[] args) {
//创建一个进程,调用Test1.java文件
ProcessBuilder pb = new ProcessBuilder("java", "Test1");
//启动进程
Process p = pb.start();
}
}
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("被调用成功!");
}
}
以TestProcess.java文件执行成功后,我们会在c盘更目录下看见一个Test1.txt文件,但是控制台并没有输出 “被调用成功!”的信息。因此可以断定,Test已经被执行成功,但因为某种原因,Test1的输出信息未在TestProcess.java的控制台中输出。这个原因也很简单,因为使用start建立的是TestProcess的子进程,这个子进程并没有自己的控制台,因此,它并不会输出任何信息。
如果要输出子进程的输出信息,可以通过Process中的getInputStream得到子进程的输出流(在子进程中输出,在父进程中就是输入),然后将子进程中的输出流从父进程的控制台输出, 具体示例代码如下。
import java.io.*;
public class TestProcess {
public static void main(String[] args) {
//创建一个进程,调用Test1.java文件
ProcessBuilder pb = new ProcessBuilder("java", "Test1");
//启动进程
Process p = pb.start();
//获取子进程的输出流
BufferedInputStream in = new BufferedInputStream(p.getInputStream());
BufferedReader br = new BufferedReader(new InputStreamReader(in));
String s;
while ((s = br.readLine()) != null){
System.out.println(s);
}
}
}
//如上操作,就可在父进程中输出子进程的输出流 "被调用成功!"
2.2、Runtime示例
TestProcess.java文件
public class TestProcess {
public static void main(String[] args) {
//获取Runtime引用对象
Runtime run = Runtime.getRuntime();
//启动进程,Test1是类名称
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);
}
}
}
以上除了输出信息,还有输入信息。既然子进程没有自己的控制台,那么输入信息也得由父进程提供。我们可以通过Process的getOutputStream方法来为子进程提供输入信息(即由父进程向子进程输入信息,而不是由控制台输入信息)。我们可以看看如下的代码:
Test1.java文件代码
public class Test {
public static void main(String[] args) {
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
System.out.println("由父进程输入的信息:" + br.readLine());
}
}
TestProcess.java文件
public class TestProcess {
public static void main(String[] args) {
//获取Runtime引用对象
Runtime run = Runtime.getRuntime();
//启动进程
Process p = run.exec("java Test1");
//向子进程输入信息
BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(p.getOutputStream()));
bw.write("向子进程输出信息");
bw.flush();
bw.close(); // 必须得关闭流,否则无法向子进程中输入信息
//获取子进程输出流
BufferedInputStream in = new BufferedInputStream(p.getInputStream());
BufferedReader br = new BufferedReader(new InputStreamReader(in));
String s;
while ((s = br.readLine()) != null) {
System.out.println(s);
}
}
}
从以上代码可以看出,Test1得到由TestProcess发过来的信息,并将其输出。当你不加bw.flash()和bw.close()时,信息将无法到达子进程,也就是说子进程进入阻塞状态,但由于父进程已经退出了,因此,子进程也跟着退出了。如果要证明这一点,可以在最后加上System.in.read(),然后通过任务管理器(在windows下)查看java进程,你会发现如果加上bw.flush()和bw.close(),只有一个java进程存在,如果去掉它们,就有两个java进程存在。这是因为,如果将信息传给Test1,在得到信息后,Test1就退出了。在这里有一点需要说明一下,exec的执行是异步的,并不会因为执行的某个程序阻塞而停止执行下面的代码。因此,可以在运行Test1后,仍可以执行下面的代码。
注意: 在建立子进程上,ProcessBuilder和Runtime类似,不同的ProcessBuilder使用start()方法启动子进程,而Runtime使用exec方法启动子进程。得到Process后,它们的操作就完全一样的。