一、多进程编程
1.1、为什么要使用多进程编程
一个 .exe 文件执行以后,就会变成一个进程.
多进程的由来:为了解决某些大型复杂问题,就需要把一个很大的任务,拆分成一个小的任务,进一步的,就需要使用 “多进程编程”,也就是床技安多个进程,每个进程分别负责其中一部分任务. 与此同时,也带来一个问题——“进程的 创建/销毁,比较重量(低效)”.
线程的由来:引入了线程,相比于 进程的 创建/销毁 更加高效,因此 Java 圈子中,大部分并发编程都是通过多线程的方式来实现.
什么情况下要使用多进程编程呢?
进程相比于线程最大的优势就是:进程的 “独立性”.
- 多线程劣势:一个操作系统上,同一时刻一个进程中运行着多个线程(共用一个地址空间),某个线程挂了,就可能把整个线程带走;
- 多进程优势:一个操作系统上,同一时刻运行着很多进程,由于不同的进程有各自的地址空间,那么即使某一个进程挂了,也不会影响到其他进程.
比如在一个 OJ 系统中,用户提交的代码就是一个独立的逻辑,整个逻辑就需要使用多进程的方式来执行. 因为用户的代码很有可能是存在问题的(一运行就崩溃),使用多线程就很有可能导致用户代码直接把整个服务器进程搞挂.
1.2、Java 中多进程编程的实现
1.2.1、前言
在操作系统的角度上(例如 Linux),提供了很多和多线程编程相关的接口,例如 进程创建、进程终止、进程等待、进程程序替换、进程间通讯.
但是在 Java 中对这些操作进行了限制,最终只提供了两个操作:进程创建 和 进程等待.
1.2.2、进程创建
通过 Runtime 实例中的 exec 方法(参数是一个字符串,相当于在 cmd 中输入了一个对应的指令)就可以创建出一个进程, 被创建出来的进程称为 “子进程”,创建子进程的进程称为 “父进程”. 咱们的服务器进程就相当于父进程,它可以有多个子进程,但是一个子进程只能有一个父进程.
一个进程在启动的时候,就会自动以下打开三个文件:
- 标准输入:对应到键盘.
- 标准输出:对应到显示器,用来正确的输出.
- 标准错误:对应到显示器,用来展示错误输出.
Ps:在 IDEA 中式看不到子进程的输出的,想要获取,可以手动获取.
例如,创建一个子进程运行 javac 命令,通过输入输出流,将子进程的 标准输出 和 错误输出 写到对应文件中.
public static void main(String[] args) throws IOException, InterruptedException {
//Runtime 再 JVM 中是一个单例
Runtime runtime = Runtime.getRuntime();
//Process 就表示进程
Process process = runtime.exec("javac");
//获取子进程的标准输出和标准错误,并写道两个文件中
//1.标准hou出
//1) 通过 标准输入流 将子进程的标准输出读出来,写入到 stdout.txt 文件中
InputStream stdoutFrom = process.getInputStream();
FileOutputStream stdoutTo = new FileOutputStream("stdout.txt");
while(true) {
int ch = stdoutFrom.read();
if(ch == -1) { //读到 EOF 为止(EOF 就是 -1)
break;
}
stdoutTo.write(ch);
}
//2) 释放文件描述符
stdoutFrom.close();
stdoutTo.close();
//2.标准错误
//2) 通过标准输入流 将子进程的标准错误读出来,写入到 stderr.txt
InputStream stderrFrom = process.getErrorStream();
FileOutputStream stderrTo = new FileOutputStream("stderr.txt");
while(true) {
int ch = stderrFrom.read();
if(ch == -1) {
break;
}
stderrTo.write(ch);
}
//2) 释放文件描述符
stderrFrom.close();
stderrTo.close();
}
运行后可以看到生成如下两个文件:
由于这里我只是单纯的输入 javac 命令(没有指定编译的 jar 包),因此是一个错误命令,那么就可以在 标准错误 中看到如下信息:
Ps:javac 往控制台输出的命令,再 windows 简体中文版系统中,默认是 gbk 编码,idea 默认式 utf8,打开后可能会乱码,因此只需要再 idea 提示中,通过 gbk 重新加载即可.
在 cmd 中输入 javac 也是一样的结果:
1.2.3、进程等待
在某些场景中,我们希望父进程等待子进程执行完毕以后,再执行后续的代码.
例如,OJ 系统就需要让用户提交代码,然后编译执行代码,再把执行结果的响应返回给用户.
具体实现如下:
通过 Process 类中的 waitFor 方法实现进程等待,父进程执行到 waitFor 的时候就会阻塞,知道子进程执行完毕为止(类似 Thread.join). 最后会返回一个退出码,表示子进程执行结果是否 ok,正常退出就返回 0,异常退出(子进程执行过程中抛异常了)就非0.
//进程等待
int exitCode = process.waitFor();
System.out.println(exitCode);
1.2.4、封装操作到一个工具类中
通常,我们会将进程创建和等待封装到一个工具类中去使用.
public class CommandUtil {
/**
* @param cmd 进程要执行的命令
* @param stdout 标准输出文件名 + 后缀
* @param stderr 标准错误文件名 + 后缀
* @return 进程等待后返回的状态码
*/
public static int run(String cmd, String stdout, String stderr) {
try {
//1.获取 Runtime 实例,执行 exec 方法
Runtime runtime = Runtime.getRuntime();
Process process = runtime.exec(cmd);
//2.获取 标准输出
if(stdout != null) {
InputStream stdoutFrom = process.getInputStream();
FileOutputStream stdoutTo = new FileOutputStream(stdout);
while(true) {
int read = stdoutFrom.read();
if(read == -1) {
break;
}
stdoutTo.write(read);
}
stdoutFrom.close();
stdoutTo.close();
}
//3.获取 标准错误
if(stderr != null) {
InputStream stderrFrom = process.getErrorStream();
FileOutputStream stderrTo = new FileOutputStream(stderr);
while(true) {
int read = stderrFrom.read();
if(read == -1) {
break;
}
stderrTo.write(read);
}
stderrFrom.close();
stderrTo.close();
}
//4.进程等待
return process.waitFor();
} catch (Exception e) {
e.printStackTrace();
}
//返回异常的状态码
return 1;
}
//测试
public static void main(String[] args) {
int result = run("javac", "stdout.txt", "stderr.txt");
System.out.println(result);
}
}