概述
进程与线程,本质意义上说, 是操作系统的调度单位,可以看成是一种操作系统 “资源” 。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)</div>
他们主要的不同在于传入命令参数的形式,提供的环境变量以及定义执行目录。
ProcessBuilder.start 方法来建立一个本地的进程
如果希望在新创建的进程中使用当前的目录和环境变量,则不需要任何配置,直接将命令行和参数传入 ProcessBuilder 中,然后调用 start 方法,就可以获得进程的引用。
Process p = new ProcessBuilder("command", "param").start();</div>
也可以先配置环境变量和工作目录,然后创建进程。
ProcessBuilder pb = new ProcessBuilder("command", "param1", "param2"); Map<String, String> env = pb.environment(); env.put("VAR", "Value"); pb.directory("Dir"); Process p = pb.start();</div>
可以预先配置 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
表 1. JDK 中 native 方法与 Windows API 的对应关系
以 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;</div>
在 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; }</div>
可以看到在创建一个进程的时候,调用 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 进程在使用的时候还有些要注意的事情:
1. Java 提供的输入输出的管道容量是十分有限的,如果不及时读取会导致进程挂起甚至引起死锁。
2. 当创建进程去执行 Windows 下的系统命令时,如:dir、copy 等。需要运行 windows 的命令解释器,command.exe/cmd.exe,这依赖于 windows 的版本,这样才可以运行系统的命令。
3. 对于 Shell 中的管道 ‘ | '命令,各平台下的重定向命令符 ‘ > ',都无法通过命令参数直接传入进行实现,而需要在 Java 代码中做一些处理,如定义新的流来存储标准输出,等等问题。
总之,Java 中对操作系统的进程进行了封装,屏蔽了操作系统进程相关的信息。同时,在使用 Java 提供创建进程运行本地命令的时候,需要小心使用。
一般而言,使用进程是为了执行某项任务,而现代操作系统对于执行任务的计算资源的配置调度一般是以线程为对象(早期的类 Unix 系统因为不支持线程,所以进程也是调度单位,但那是比较轻量级的进程,在此不做深入讨论)。创建一个进程,操作系统实际上还是会为此创建相应的线程以运行一系列指令。特别地,当一个任务比较庞大复杂,可能需要创建多个线程以实现逻辑上并发执行的时候,线程的作用更为明显。因而我们有必要深入了解 Java 中的线程,以避免可能出现的问题。本文下面的内容即是呈现 Java 线程的创建方式以及它与操作