(转)java中的进程与线程
(转自地址http://www.ibm.com/developerworks/cn/java/j-lo-processthread/)
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 中调用方法时的传入的参数传递给操作系统对应的方法来实现相应的功能。如表 1
表 1. JDK 中 native 方法与 Windows API 的对应关系
JDK 中调用的 native 方法名 | 对应调用的 Windows API |
---|---|
create | CreateProcess,CreatePipe |
close | CloseHandle |
waitfor | WaitForMultipleObjects |
destroy | TerminateProcess |
exitValue | GetExitCodeProcess |
以 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 所示 。
清单 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 进程在使用的时候还有些要注意的事情:
- Java 提供的输入输出的管道容量是十分有限的,如果不及时读取会导致进程挂起甚至引起死锁。
- 当创建进程去执行 Windows 下的系统命令时,如:dir、copy 等。需要运行 windows 的命令解释器,command.exe/cmd.exe,这依赖于 windows 的版本,这样才可以运行系统的命令。
- 对于 Shell 中的管道 ‘ | ’命令,各平台下的重定向命令符 ‘ > ’,都无法通过命令参数直接传入进行实现,而需要在 Java 代码中做一些处理,如定义新的流来存储标准输出,等等问题。
总之,Java 中对操作系统的进程进行了封装,屏蔽了操作系统进程相关的信息。同时,在使用 Java 提供创建进程运行本地命令的时候,需要小心使用。
一般而言,使用进程是为了执行某项任务,而现代操作系统对于执行任务的计算资源的配置调度一般是以线程为对象(早期的类 Unix 系统因为不支持线程,所以进程也是调度单位,但那是比较轻量级的进程,在此不做深入讨论)。创建一个进程,操作系统实际上还是会为此创建相应的线程以运行一系列指令。特别地,当一个任务比较庞大复杂,可能需要创建多个线程以实现逻辑上并发执行的时候,线程的作用更为明显。因而我们有必要深入了解 Java 中的线程,以避免可能出现的问题。本文下面的内容即是呈现 Java 线程的创建方式以及它与操作系统线程的联系与区别。
Java 创建线程的方法
实际上,创建线程最重要的是提供线程函数(回调函数),该函数作为新创建线程的入口函数,实现自己想要的功能。Java 提供了两种方法来创建一个线程:
- 继承 Thread 类
class MyThread extends Thread{
public void run() {
System.out.println("My thread is started.");
}
}
实现该继承类的 run 方法,然后就可以创建这个子类的对象,调用 start 方法即可创建一个新的线程:
MyThread myThread = new MyThread();
myThread.start();
- 实现 Runnable 接口
class MyRunnable implements Runnable{
public void run() {
System.out.println("My runnable is invoked.");
}
}
实现 Runnable 接口的类的对象可以作为一个参数传递到创建的 Thread 对象中,同样调用 Thread#start 方法就可以在一个新的线程中运行 run 方法中的代码了。
Thread myThread = new Thread( new MyRunnable());
myThread.start();
可以看到,不管是用哪种方法,实际上都是要实现一个 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 所示。
清单 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 所示。
清单 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 没有提供方法来获取线程的退出返回值。实际上,线程可以有退出返回值,它一般被操作系统存储在线程控制结构中 (TCB),调用者可以通过检测该值来确定线程是正常退出还是异常终止。
线程的同步
Java 提供方法 Thread#Join()来等待一个线程结束,一般情况这就足够了,但一种可能的情况是,需要等待在多个线程上(比如任意一个线程结束或者所有线程结束才会返回),循环调用每个线程的 Join 方法是不可行的,这可能导致很奇怪的同步问题。
线程的 ID
Java 提供的方法 Thread#getID()返回的是一个简单的计数 ID,其实和操作系统线程的 ID 没有任何关系。
线程运行时间统计
Java 没有提供方法来获取线程中某段代码的运行时间的统计结果。虽然可以自行使用计时的方法来实现(获取运行开始和结束的时间,然后相减 ),但由于存在多线程调度方法的原因,无法获取线程实际使用的 CPU 运算时间,因而必然是不准确的。
总结
本文通过对 Java 进程和线程的分析,可以看出 Java 对这两种操作系统 “资源” 进行了封装,使得开发人员只需关注如何使用这两种 “资源” ,而不必过多的关心细节。这样的封装一方面降低了开发人员的工作复杂度,提高了工作效率;另一方面由于封装屏蔽了操作系统本身的一些特性,因而在使用 Java 进程线程时有了某些限制,这是封装不可避免的问题。语言的演化本就是决定需要什么不需要什么的过程,相信随着 Java 的不断发展,封装的功能子集的必然越来越完善。
(转)java中的进程与线程的更多相关文章
- Java中的进程和线程
Java中的进程与线程 一:进程与线程 概述:几乎任何的操作系统都支持运行多个任务,通常一个任务就是一个程序,而一个程序就是一个进程.当一个进程运行时,内部可能包括多个顺序执行流,每个顺序执行流就是 ...
- Java中的进程与线程(总结篇)
详细文档: Java中的进程与线程.rar 474KB 1/7/2017 6:21:15 PM 概述: 几乎任何的操作系统都支持运行多个任务,通常一个任务就是一个程序,而一个程序就是一个进程.当一个进 ...
- java中的进程与线程及java对象的内存结构【转】
原文地址:http://rainforc.iteye.com/blog/2039501 1.实现线程的三种方式: 使用内核线程实现 内核线程(Kernel Thread, KLT)就是 ...
- python中的进程、线程(threading、multiprocessing、Queue、subprocess)
Python中的进程与线程 学习知识,我们不但要知其然,还是知其所以然.你做到了你就比别人NB. 我们先了解一下什么是进程和线程. 进程与线程的历史 我们都知道计算机是由硬件和软件组成的.硬件中的CP ...
- java 获取当前进程id 线程id
java 获取当前进程id 线程id RuntimeMXBean (Java Platform SE 8 ) https://docs.oracle.com/javase/8/docs/api/j ...
- windows中的进程和线程
今天咱们就聊聊windows中的进程和线程 2016-09-30 在讨论windows下的进程和线程时,我们先回顾下通用操作系统的进程和线程.之所以称之为通用是因为一贯的本科或者其他教材都是这么说的: ...
- c#Winform程序调用app.config文件配置数据库连接字符串 SQL Server文章目录 浅谈SQL Server中统计对于查询的影响 有关索引的DMV SQL Server中的执行引擎入门 【译】表变量和临时表的比较 对于表列数据类型选择的一点思考 SQL Server复制入门(一)----复制简介 操作系统中的进程与线程
c#Winform程序调用app.config文件配置数据库连接字符串 你新建winform项目的时候,会有一个app.config的配置文件,写在里面的<connectionStrings n ...
- JAVA 中无锁的线程安全整数 AtomicInteger介绍和使用
Java 中无锁的线程安全整数 AtomicInteger,一个提供原子操作的Integer的类.在Java语言中,++i和i++操作并不是线程安全的,在使用的时候, 不可避免的会用到synchron ...
- java中的++i是线程安全的吗?
java中的++i是线程安全的吗?为什么?怎么使它线程安全呢? 先说答案: 非线程安全 先说下为什么是非线程安全的? 从Java内存模型说起 Java内存模型规定了所有的便利都存储在主内存中,每个线程 ...
随机推荐
- C#,委托,匿名委托,Lambda表达式
1. 委托是什么? (1) 从数据结构来讲,委托是和类一样是一种用户自定义类型. (2) 从设计模式来讲,委托(类)提供了方法(对象)的抽象.概括的说:委托是方法的抽象. 2. 委托类型的定义: 委 ...
- CSS z-index 属性
定义和用法 z-index 属性设置元素的堆叠顺序.拥有更高堆叠顺序的元素总是会处于堆叠顺序较低的元素的前面. 注释:元素可拥有负的 z-index 属性值. 注释:Z-index 仅能在定位元素上奏 ...
- [原创.数据可视化系列之六]使用openlyaers进行公网地图剪切
进行地图开发的过程中,我一般使用天地图或者微软的地图作为地图,因为这两种地图的经纬度偏差最小,基本可以满足用户需求,比如: 不用说,都是全部地图,这也是最常用的一种方法. 但是用户说,我只看大连的地图 ...
- 形象的讲解angular中的$q与promise(转)
以下内容摘自http://www.ngnice.com/posts/126ee9cf6ddb68 promise不是angular首创的,作为一种编程模式,它出现在……1976年,比js还要古老得多. ...
- vim 标记 mark 详解 (转载)
http://www.cnblogs.com/jianyungsun/archive/2011/02/14/1954057.html Vim 允许你在文本中放置自定义的标记.命令 "ma&q ...
- Oracle中的正则表达式
检查约束 --密码的长度必须在3-6 --年龄必须在1-120 --性别只能是男或女 --电话号码必须满足电话的格式: 手机格式,座机格式 drop table test; select * from ...
- 什么是遗传方差(Genetic variance)、加性遗传方差(Additive genetic variance)、显性遗传方差(Dominance genetic variance)、上位遗传方差(Epistatic genetic variance)
遗传方差:遗传方差又称表型方差(phenotypic variance),通常结合基因型方差(genotype variance)和环境方差(environmental variance).遗传方差主 ...
- Xcode导航栏不显示模拟器选择框ToolBar
不显示ToolBar的小伙伴可能就是下面的样子: 全屏后就可以看到ToolBar,像下面这样: 刚开始还以为是模拟器没装,还傻不拉几的去下载模拟器,后来才发现,只要下面的操作即可显示 点击" ...
- textFiled的placeHolder字体颜色
self.title=@"修改UITextField的placeholder字体颜色"; UITextField *textTF=[[UITextField alloc]initW ...
- 【经验】ansible 批量推送公钥
1.使用 ssh-keygen -t rsa生成密钥对 ssh-keygen -t rsa 2.推送单个公钥到远程机器 格式: ssh-copy-id -i ~/.ssh/id_rsa.pub use ...