文档章节

Java中的进程与线程

秋风醉了
 秋风醉了
发布于 2016/09/21 19:06
字数 3847
阅读 93
收藏 5

Java中的进程与线程

概念

进程与线程,本质意义上说, 是操作系统的调度单位,可以看成是一种操作系统 “资源” 。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)

他们主要的不同在于传入命令参数的形式,提供的环境变量以及定义执行目录。

ProcessBuilder.start 方法来建立一个本地的进程

如果希望在新创建的进程中使用当前的目录和环境变量,则不需要任何配置,直接将命令行和参数传入 ProcessBuilder 中,然后调用 start 方法,就可以获得进程的引用。

 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 方法创建的进程,对修改之前创建的进程实例没有影响。

JVM 对进程的实现

在 JDK 的代码中,只提供了 ProcessImpl 类来实现 Process 抽象类。其中引用了 native 的 create, close, waitfor, destory 和 exitValue 方法。在 Java 中,native 方法是依赖于操作系统平台的本地方法,它的实现是用 C/C++ 等类似的底层语言实现。我们可以在 JVM 的源代码中找到对应的本地方法,然后对其进行分析。JVM 对进程的实现相对比较简单,以 Windows 下的 JVM 为例。在 JVM 中,将 Java 中调用方法时的传入的参数传递给操作系统对应的方法来实现相应的功能。

以 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;

在 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 进程与操作系统进程

通过上面对 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 线程的创建方式以及它与操作系统线程的联系与区别。

Java 创建线程的方法

实际上,创建线程最重要的是提供线程函数(回调函数),该函数作为新创建线程的入口函数,实现自己想要的功能。Java 提供了两种方法来创建一个线程:

  • 继承 Thread 类
  • 实现 Runnable 接口

不管是用哪种方法,实际上都是要实现一个 run 方法的。 该方法本质是上一个回调方法。由 start 方法新创建的线程会调用这个方法从而执行需要的代码。 从后面可以看到,run 方法并不是真正的线程函数,只是被线程函数调用的一个 Java 方法而已,和其他的 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 开发者提供的接口存在以下的局限性:

  1. 线程返回值 Java 没有提供方法来获取线程的退出返回值。实际上,线程可以有退出返回值,它一般被操作系统存储在线程控制结构中 (TCB),调用者可以通过检测该值来确定线程是正常退出还是异常终止。
  2. 线程的同步 Java 提供方法 Thread#Join()来等待一个线程结束,一般情况这就足够了,但一种可能的情况是,需要等待在多个线程上(比如任意一个线程结束或者所有线程结束才会返回),循环调用每个线程的 Join 方法是不可行的,这可能导致很奇怪的同步问题。
  3. 线程的 ID Java 提供的方法 Thread#getID()返回的是一个简单的计数 ID,其实和操作系统线程的 ID 没有任何关系。
  4. 线程运行时间统计 Java 没有提供方法来获取线程中某段代码的运行时间的统计结果。虽然可以自行使用计时的方法来实现(获取运行开始和结束的时间,然后相减 ),但由于存在多线程调度方法的原因,无法获取线程实际使用的 CPU 运算时间,因而必然是不准确的。

总结

本文通过对 Java 进程和线程的分析,可以看出 Java 对这两种操作系统 “资源” 进行了封装,使得开发人员只需关注如何使用这两种 “资源” ,而不必过多的关心细节。这样的封装一方面降低了开发人员的工作复杂度,提高了工作效率;另一方面由于封装屏蔽了操作系统本身的一些特性,因而在使用 Java 进程线程时有了某些限制,这是封装不可避免的问题。语言的演化本就是决定需要什么不需要什么的过程,相信随着 Java 的不断发展,封装的功能子集的必然越来越完善。

========END========

本文转载自:https://www.ibm.com/developerworks/cn/java/j-lo-processthread/

秋风醉了
粉丝 252
博文 532
码字总数 405694
作品 0
朝阳
程序员
私信 提问
好程序员Java分享JVM结构

  好程序员Java分享JVM结构,jvm的基本结构,也就是我们俗称概述。内容很多,而且概念量也很大,关于概念方面,让概念在你的脑子里变成图形,所以只要你有耐心、仔细,发挥自己的想象力,会...

好程序员IT
05/31
87
0
java核心技术-多线程之线程基础

说起线程,无法免俗首先要弄清楚的三个概念就是:进程、线程、协程。OK,那什么是进程,什么是线程,哪协程又是啥东西。进程:进程可以简单的理解为运行在操作系统中的程序,程序时静态代码,...

xgoing
2018/08/19
0
0
java:找出占用CPU资源最多的那个线程(HOW TO)

在这里对linux下、sun(oracle) JDK的线程资源占用问题的查找步骤做一个小结;linux环境下,当发现java进程占用CPU资源很高,且又要想更进一步查出哪一个java线程占用了CPU资源时,按照以下步...

鉴客
2012/06/28
10.3K
3
Java 对象锁-synchronized()与线程的状态与生命周期与守护进程

synchronized(someObject){ //对象锁} 一、对象锁 someObject 的使用说明: 1、对象锁的返还。 当synchronize()语句执行完成。 当synchronize()语句执行出现异常。 当线程调用了wait()方法。...

Oscarfff
2015/05/04
941
0
Linux ---> 监控JVM工具

JDK内置工具使用 jps(Java Virtual Machine Process Status Tool) 查看所有的jvm进程,包括进程ID,进程启动的路径等等。 jstack(Java Stack Trace) ① 观察jvm中当前所有线程的运行情况和线...

shking
2013/10/10
5.8K
0

没有更多内容

加载失败,请刷新页面

加载更多

新建时隐藏按钮,显示明细时显示

在InitControl()中 if (saTableKeys != null) { rpgDesign.Visible = true; rpgPrint.Visible = true; }......

_Somuns
35分钟前
5
0
【实战演练,拒绝996】-SpringBoot2.x自定义Spring boot Starter

欢迎关注 提升能力,涨薪可待 面试知识,工作可待 实战演练,拒绝996 如果此文对你有帮助、喜欢的话,那就点个赞呗! 前言 是不是感觉在工作上难于晋升了呢? 是不是感觉找工作面试是那么难呢...

ccww_
37分钟前
10
0
SpringBoot从入门到放弃,原理篇-自动配置原理

SpringBoot从入门到放弃,原理篇-自动配置原理 springboot自动配置原理 配置文件能配置的属性参照 自动配置原理 1、springboot启动的时候加载主配置类,开启了自动配置功能@EnableAutoConfig...

有一个小阿飞
今天
11
0
php变量和数据类型

php中的变量 PHP中的变量声明 PHP中的变量的使用 PHP中的数据类型之整型 PHP数据类型之浮点类型和布尔类型 PHP数据类型之字符串类型 PHP数据类型之heredoc和nowdoc的使用 PHP数据类型之复合类...

达达前端小酒馆
今天
7
0
OSChina 周日乱弹 —— 沙发忽然就爆炸了,吓死我了

Osc乱弹歌单(2019)请戳(这里) 【今日歌曲】@这次装个文艺青年吧:#今日歌曲推荐# 分享Vicetone/Youngblood Hawke的单曲《Landslide》: 《Landslide》- Vicetone/Youngblood Hawke 手机党...

小小编辑
今天
253
10

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部