Java中的进程与线程程序员
进程与线程,本质意义上说, 是操做系统的调度单位,能够当作是一种操做系统 “资源” 。Java 做为与平台无关的编程语言,必然会对底层(操做系统)提供的功能进行进一步的封装,以平台无关的编程接口供程序员使用,进程与线程做为操做系统核心概念的一部分无疑亦是如此。在 Java 语言中,对进程和线程的封装,分别提供了 Process 和 Thread 相关的一些类。本文首先简单的介绍如何使用这些类来建立进程和线程,而后着重介绍这些类是如何和操做系统本地进程线程相对应的,给出了 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 引用。windows
该方法在 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)
他们主要的不一样在于传入命令参数的形式,提供的环境变量以及定义执行目录。并发
若是但愿在新建立的进程中使用当前的目录和环境变量,则不须要任何配置,直接将命令行和参数传入 ProcessBuilder 中,而后调用 start 方法,就能够得到进程的引用。jvm
Process p = new ProcessBuilder("command", "param").start();
也能够先配置环境变量和工做目录,而后建立进程。编程语言
ProcessBuilder pb = new ProcessBuilder("command", "param1", "param2"); Map<String, String> env = pb.environment(); env.put("VAR", "Value"); pb.directory("Dir"); Process p = pb.start();
能够预先配置 ProcessBuilder 的属性是经过 ProcessBuilder 建立进程的最大优势。并且能够在后面的使用中随着须要去改变代码中 pb 变量的属性。若是后续代码修改了其属性,那么会影响到修改后用 start 方法建立的进程,对修改以前建立的进程实例没有影响。函数
在 JDK 的代码中,只提供了 ProcessImpl 类来实现 Process 抽象类。其中引用了 native 的 create, close, waitfor, destory 和 exitValue 方法。在 Java 中,native 方法是依赖于操做系统平台的本地方法,它的实现是用 C/C++ 等相似的底层语言实现。咱们能够在 JVM 的源代码中找到对应的本地方法,而后对其进行分析。JVM 对进程的实现相对比较简单,以 Windows 下的 JVM 为例。在 JVM 中,将 Java 中调用方法时的传入的参数传递给操做系统对应的方法来实现相应的功能。ui
以 create 方法为例,咱们看一下它是如何和系统 API 进行链接的。 在 ProcessImple 类中,存在 native 的 create 方法,其参数以下:spa
private native long create(String cmdstr, String envblock, String dir, boolean redirectErrorStream, FileDescriptor in_fd, FileDescriptor out_fd, FileDescriptor err_fd) throws IOException;
在 JVM 中对应的本地方法如代码清单 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; }
能够看到在建立一个进程的时候,调用 Windows 提供的 CreatePipe 方法创建输入,输出和错误管道,同时将用户经过 Java 传入的参数转换为操做系统能够识别的 C 语言的格式,而后调用 Windows 提供的建立系统进程的方式,建立一个进程,同时在 JAVA 虚拟机中保存了这个进程对应的句柄,而后返回给了 ProcessImpl 类,可是该类将返回句柄进行了隐藏。也正是 Java 跨平台的特性体现,JVM 尽量的将和操做系统相关的实现细节进行了封装,并隐藏了起来。 一样,在用户调用 close、waitfor、destory 以及 exitValue 方法之后, JVM 会首先取得以前保存的该进程在操做系统中的句柄,而后经过调用操做系统提供的接口对该进程进行操做。经过这种方式来实现对进程的操做。 在其它平台下也是用相似的方式实现的,不一样的是调用的对应平台的 API 会有所不一样。
经过上面对 Java 进程的分析,其实它在实现上就是建立了操做系统的一个进程,也就是每一个 JVM 中建立的进程都对应了操做系统中的一个进程。可是,Java 为了给用户更好的更方便的使用,向用户屏蔽了一些与平台相关的信息,这为用户须要使用的时候,带来了些许不便。 在使用 C/C++ 建立系统进程的时候,是能够得到进程的 PID 值的,能够直接经过该 PID 去操做相应进程。可是在 JAVA 中,用户只能经过实例的引用去进行操做,当该引用丢失或者没法取得的时候,就没法了解任何该进程的信息。
固然,Java 进程在使用的时候还有些要注意的事情:
总之,Java 中对操做系统的进程进行了封装,屏蔽了操做系统进程相关的信息。同时,在使用 Java 提供建立进程运行本地命令的时候,须要当心使用。
通常而言,使用进程是为了执行某项任务,而现代操做系统对于执行任务的计算资源的配置调度通常是以线程为对象(早期的类 Unix 系统由于不支持线程,因此进程也是调度单位,但那是比较轻量级的进程,在此不作深刻讨论)。建立一个进程,操做系统实际上仍是会为此建立相应的线程以运行一系列指令。特别地,当一个任务比较庞大复杂,可能须要建立多个线程以实现逻辑上并发执行的时候,线程的做用更为明显。于是咱们有必要深刻了解 Java 中的线程,以免可能出现的问题。本文下面的内容便是呈现 Java 线程的建立方式以及它与操做系统线程的联系与区别。
实际上,建立线程最重要的是提供线程函数(回调函数),该函数做为新建立线程的入口函数,实现本身想要的功能。Java 提供了两种方法来建立一个线程:
不论是用哪一种方法,实际上都是要实现一个 run 方法的。 该方法本质是上一个回调方法。由 start 方法新建立的线程会调用这个方法从而执行须要的代码。 从后面能够看到,run 方法并非真正的线程函数,只是被线程函数调用的一个 Java 方法而已,和其余的 Java 方法没有什么本质的不一样。
从概念上来讲,一个 Java 线程的建立根本上就对应了一个本地线程(native thread)的建立,二者是一一对应的。 问题是,本地线程执行的应该是本地代码,而 Java 线程提供的线程函数是 Java 方法,编译出的是 Java 字节码,因此能够想象的是, Java 线程其实提供了一个统一的线程函数,该线程函数经过 Java 虚拟机调用 Java 线程方法 , 这是经过 Java 本地方法调用来实现的。
如下是 Thread#start 方法的示例:
public synchronized void start() { … start0(); … }
能够看到它实际上调用了本地方法 start0, 该方法的声明以下:
private native void start0();
Thread 类有个 registerNatives 本地方法,该方法主要的做用就是注册一些本地方法供 Thread 类使用,如 start0(),stop0() 等等,能够说,全部操做本地线程的本地方法都是由它注册的 . 这个方法放在一个 static 语句块中,这就代表,当该类被加载到 JVM 中的时候,它就会被调用,进而注册相应的本地方法。
private static native void registerNatives(); static{ registerNatives(); }
本地方法 registerNatives 是定义在 Thread.c 文件中的。Thread.c 是个很小的文件,定义了各个操做系统平台都要用到的关于线程的公用数据和操做,如代码清单 2 所示。
JNIEXPORT void JNICALL Java_Java_lang_Thread_registerNatives (JNIEnv *env, jclass cls){ (*env)->RegisterNatives(env, cls, methods, ARRAY_LENGTH(methods)); } static JNINativeMethod methods[] = { {"start0", "()V",(void *)&JVM_StartThread}, {"stop0", "(" OBJ ")V", (void *)&JVM_StopThread}, {"isAlive","()Z",(void *)&JVM_IsThreadAlive}, {"suspend0","()V",(void *)&JVM_SuspendThread}, {"resume0","()V",(void *)&JVM_ResumeThread}, {"setPriority0","(I)V",(void *)&JVM_SetThreadPriority}, {"yield", "()V",(void *)&JVM_Yield}, {"sleep","(J)V",(void *)&JVM_Sleep}, {"currentThread","()" THD,(void *)&JVM_CurrentThread}, {"countStackFrames","()I",(void *)&JVM_CountStackFrames}, {"interrupt0","()V",(void *)&JVM_Interrupt}, {"isInterrupted","(Z)Z",(void *)&JVM_IsInterrupted}, {"holdsLock","(" OBJ ")Z",(void *)&JVM_HoldsLock}, {"getThreads","()[" THD,(void *)&JVM_GetAllThreads}, {"dumpThreads","([" THD ")[[" STE, (void *)&JVM_DumpThreads}, };
到此,能够容易的看出 Java 线程调用 start 的方法,实际上会调用到 JVM_StartThread 方法,那这个方法又是怎样的逻辑呢。实际上,咱们须要的是(或者说 Java 表现行为)该方法最终要调用 Java 线程的 run 方法,事实的确如此。 在 jvm.cpp 中,有以下代码段:
JVM_ENTRY(void, JVM_StartThread(JNIEnv* env, jobject jthread)) … native_thread = new JavaThread(&thread_entry, sz); …
这里JVM_ENTRY是一个宏,用来定义JVM_StartThread 函数,能够看到函数内建立了真正的平台相关的本地线程,其线程函数是 thread_entry,如清单 3 所示。
static void thread_entry(JavaThread* thread, TRAPS) { HandleMark hm(THREAD); Handle obj(THREAD, thread->threadObj()); JavaValue result(T_VOID); JavaCalls::call_virtual(&result,obj, KlassHandle(THREAD,SystemDictionary::Thread_klass()), vmSymbolHandles::run_method_name(), vmSymbolHandles::void_method_signature(),THREAD); }
能够看到调用了 vmSymbolHandles::run_method_name 方法,这是在 vmSymbols.hpp 用宏定义的:
class vmSymbolHandles: AllStatic { … template(run_method_name,"run") … }
至于 run_method_name 是如何声明定义的,由于涉及到很繁琐的代码细节,本文不作赘述。感兴趣的读者能够自行查看 JVM 的源代码。 图 1. Java 线程建立调用关系图
综上所述,Java 线程的建立调用过程如 图 1 所示,首先 , Java 线程的 start 方法会建立一个本地线程(经过调用 JVM_StartThread),该线程的线程函数是定义在 jvm.cpp 中的 thread_entry,由其再进一步调用 run 方法。能够看到 Java 线程的 run 方法和普通方法其实没有本质区别,直接调用 run 方法不会报错,可是倒是在当前线程执行,而不会建立一个新的线程。
从上咱们知道,Java 线程是创建在系统本地线程之上的,是另外一层封装,其面向 Java 开发者提供的接口存在如下的局限性:
本文经过对 Java 进程和线程的分析,能够看出 Java 对这两种操做系统 “资源” 进行了封装,使得开发人员只需关注如何使用这两种 “资源” ,而没必要过多的关心细节。这样的封装一方面下降了开发人员的工做复杂度,提升了工做效率;另外一方面因为封装屏蔽了操做系统自己的一些特性,于是在使用 Java 进程线程时有了某些限制,这是封装不可避免的问题。语言的演化本就是决定须要什么不须要什么的过程,相信随着 Java 的不断发展,封装的功能子集的必然愈来愈完善。
========END========