java线程销毁_Java 线程(1)- 创建与销毁

Java 采用 thread-per-task 的线程模型,即一个任务(一段代码)对应一个 Java 线程(thread),而一个 Java 线程对应一个操作系统线程,所以了解一些操作系统进程的管理知识可以更好的了解 Java 线程,下面以 Liunx 为例来分析 Java 线程

Liunx 的进程管理

Linux 中的进程(Process/Task)是正在执行的程序代码集合,是内核的最小资源调度单位,各个进程之间的资源相互隔离,Linux 通过虚拟处理器和内存,使得进程感觉自己在独享处理器和内存。而线程(Thread)是最小的计算单位,各个线程之间共享资源,理论上线程由进程产生,一个进程由若干线程组成。

Linux 中,内核不区分进程与线程,只不过使用不同的创建方式,进程由 fork() 或 exec() 系统调用创建,而线程由 clone() 系统调用创建,并且通过指定 clone 参数,与父进程共享内存空间,文件句柄,信号处理等(其本质还是进程)POSIX thread (pthread) 的实现,在 2.6之前,Linux kernel 没有真正的 thread 支持,线程库都是通过 clone() 实现的。2.6 之后,由 NPTL(Native POSIX Thread Library) 实现,NPTL 除了使用 clone() 外,在内核中增加了 futex(fast userspace mutex) 用于支持处理线程之间的 sleep 与 wake。futex 是一种高效的对共享资源互斥访问的算法。NPTL是一个1×1 的线程模型,即一个用户线程对应一个内核进程。其他一些操作系统比如 Solaris 则是 MxN 的,M 对应创建的线程数,N 对应内核可以运行的实体。NPTL 创建线程的效率非常高。一些测试显示,基于 NPTL 的内核创建 10 万个线程只需要 2 秒,而没有 NPTL 支持的内核则需要长达 15 分钟

进程的数据结构

一个进程由结构 task_struct 表示,各进程通过双向列表联系起来,创建进程时由内核为 task_struct 分配内存(32bit 下为 1.7KB ),Linux 使用 PID 标识进程 ,其最大值决定了某一时刻系统最大的并发进程的数量

进程的状态迁移

进程的创建与销毁

fork() // or exec(), clone()wait4()

exit()

无论是 fork() 还是 exec(),都是通过调用 clone() 实现的,clone() 会调用 do_fork() 完成进程的创建,clone() 可以接受一系列参数来指明需要共享的资源

Linux 中的内核线程(ps 命令中中括号标识的进程)由内核直接创建,没有独立的地址空间,只在内核空间运行。 而用户进程(执行一个可执行文件)先由 fork() 通过复制当前进程创建一个子进程,调用 fork() 的进程称为父进程,新产生的进程称为子进程。当 fork() 调用结束,在返回点上父进程恢复执行,子进程开始执行,fork() 采用写时复制,初始时父进程与子进程只有 PID 不同,然后由 exec() 读取可执行文件并将其载入地址空间开始运行(替换子进程),父进程可以通过 wait4() 系统调用(wait/waitpid)查询子进程是否终止。如果父进程在子进程之前结束(没有 wait 其子进程),子进程就可能变为僵尸进程,导致进程描述符所占的空间无法释放,现在的内核(2.6)中如果子进程的父进程提前结束,内核会为该子进程查找一个养父进程,内核先在同组进程中找,如果找不到则由 init 进程充当,然后由其养父进程释放该进程,因为 init 进程一定会 wait 其子进程,所以可以认为该进程最终一定会被释放而避免产生僵尸进程

与进程相关的内核参数

ulimit -u # max_user_processes

/proc/sys/vm/max_map_count # max_map_count

/proc/sys/kernel/threads-max # max_threads

/proc/sys/kernel/pid_max # pid_max

查看系统的线程信息

/proc//status

/proc//sched

/proc//task

# ruser 用户 ID

# lwp (light weight process) 线程ID

# psr 为 CPU 的序号

ps -eo ruser,pid,ppid,lwp,psr -L

创建 Java 线程

当我们调用 new Thread() 时,JVM 并不会立即创建一个与其对应的系统线程,而是当调用了 start() 方法之后,JVM 才会通过系统调用 clone 来创建一个与其对应的系统线程(参考 pthread_create()),因为 Java 线程最终被映射为系统线程,所以当我们需要创建线程时,尤其是需要大量线程时,我们需要注意:操作系统对线程的数量的限制

创建、调度和终止线程的系统开销

线程本身对系统资源的消耗(尤其是内存,JVM 需要为每个线程维护一个独立的线程栈 -Xss)

如果 JVM 无法创建线程,会抛出 java.lang.OutOfMemoryError: unable to create new native thread 异常

由于线程不可以无限制的使用,所以利用线程池(Executor Framework)对线程进行复用和管理是常见的使用线程的方式go 语言则采取了另外一种方式构建线程:goroutine,goroutine 构建在系统线程的基础之上,与系统线程的关系是 m:n,即在 n 个系统线程(GOMAXPROCS)上多工的调度 m 个 goroutines,因此 goroutine 使用了非常小的调用栈(2KB)并且缩短了线程之间的调度(切换)时间

创建本地进程(native process)

Java 通过 ProcessBuilder.start() (推荐)或 Runtime.exec 方法创建本地进程(即执行外部程序)并返回 Process 实例,该实例可用于控制进程并获取进程的相关信息,同时通过该实列还可以操作进程的输入输出、等待进程完成、检查进程的退出状态以及销毁(终止)进程。因为 Java 本地进程是和平台相关的,因此在使用时需要注意的地方包括:即使没有任何对当前本地进程 Process 对象的引用,本地进程也不会被终止,而是继续异步的执行

在本地进程退出之前,无法获取进程的退出状态,需要通过 Process.waitFor() 来等待外部程序的退出

创建的本地进程(子进程)没有自己的终端或控制台(Java7 之后可以通过 ProcessBuilder.inheritIO() 将子进程的标准 I/O 和当前的 Java 进程设置成一样),本地进程的所有标准 I/O(stdin,stdout 和 stderr)操作以流的方式被重定向到当前 Java 进程(父进程),在当前 Java 进程中可以通过 Process.getOutputStream(),Process.getInputStream() 和 Process.getErrorStream() 来获得这些流,然后父进程使用这些流向子进程提供输入并从子进程获取输出。由于某些本地平台仅为标准输入和输出流提供有限的缓冲区大小,因此无法及时写入输入流或读取输出流,这有可能导致子进程阻塞甚至死锁,因此需要立即处理来自本地进程的输入和输出,即在 ProcessBuilder.start() 之后,立即启动线程处理标准 I/O

不能像命令行一样在本地进程中使用管道,如果需要使用管道,需要用 shell 对其包装,如:new ProcessBuilder("/bin/sh", "-c", "ls -l| grep foo");

终止 Java 线程

Java 中,一个线程没有办法直接终止另一个线程,只能通过发送中断请求(或信号)来请求其它线程终止,而接收到请求的线程可能立即退出,也可能不做任何响应,所以一个线程只会在以下情况下退出线程的 run 方法退出,包括正常退出和异常退出

线程响应了中断请求(或终止信号),退出 run 方法

线程所在的 JVM 关闭

对于异常退出的线程,记录异常信息对以后的调试和分析都非常重要,因此要尽可能的记录线程的异常信息

public void run() {

Throwable thrown = null;

try {

while (!isInterrupted())

runTask(getTaskFromWorkQueue());

} catch (Throwable e) { //cath unexpected exception thrown = e;

} finally {

threadExited(this, thrown); // To call uncaughtException method to record the exception}

public void uncaughtException(Thread t, Throwable e) {

Logger logger = Logger.getAnonymousLogger();

logger.log(Level.SEVERE, "Thread terminated with exception: " + t.getName(), e);

}

中断(Interrupt)机制

Java 的中断机制是一种通知机制,通过中断并不能直接终止另一个线程,而只是给另一线程发送了中断请求(改变线程的中断状态),该线程自己处理该中断请求,可能立即退出,也可能不做任何响应。

中断状态

Java 每个线程都维护着一个中断状态(interrupted status),该状态代表着是否有中断请求,该状态可以被任意线程设置,包括被中断的线程本身(除了线程本身正在中断中)最佳实践:线程的中断状态应该由线程的创建者来设置

线程中与中断状态相关的方法:清除当前线程的中断状态,如果连续两次调用该方法,则第二次调用将返回 false(第一次调用已清除了其中断状态)

中断使用的场景某个操作超过了一定的执行时间限制需要中止时

多个线程做相同的事情,只要一个线程成功其它线程都可以取消时

一组线程中的一个或多个出现错误导致整组都无法继续时

当一个应用或服务需要停止时

中断的处理

想要处理中断,首先需要监测中断状态,可以不断的通过调用 interrupted 或 isInterrupted 来测试线程的中断的状态,但需要根据实际情况来决定测试频率,频繁的测试可能会使程序执行效率下降,相反,测试的较少又可能导致中断请求得不到及时响应。

当监测到中断状态后,需要选择在合适的时机处理中断,合适的时机并不一定是立即处理终端退出程序,该时机也需要根据实际情况来决定,主要是要避免线程所处理对象处于不一致状态。

当时机合适就可以处理中断,比如:回滚当前事务、清理资源、清理中断标志,抛出 InterruptedException 或立即退出。

InterruptedException 的处理Java API 中的阻塞方法一般都会声明抛出 InterruptedException 异常(代表该方法是可中断的,即会响应中断请求),Java 的阻塞线程通过清除中断状态并抛出 InterruptedException 来响应中断

如果应用程序捕获到了 InterruptedException 异常,则说明当前的线程调用的阻塞方法发生了中断,当前线程可以:选择退出或结束

继续向方法调用栈的上层抛出该异常

捕获可中断方法的 InterruptedException 并设置中断状态,表明当前线程已经中断

/*Non-cancelable Task that Restores Interruption Before Exit.*/

public Task getNextTask(BlockingQueue

boolean interrupted = false;

try {

while (true) {

try {

return queue.take();

} catch (InterruptedException e) {

interrupted = true;

// fall through and retry }

}

} finally {

if (interrupted)

Thread.currentThread().interrupt();

}

}

关闭 JVM

JVM 的关闭方式包括:通过调用 System.exit() 或键入 Ctrl-C(等同于 kill -2,向 JVM 发送 SIGINT 信号)的标准关闭

通过 kill 命令的其它参数的关闭(abrupt shutdown)

在 JVM 关闭的时候可以通过 Shutdown Hooks (Runtime.addShutdownHook)来执行一些清理工作,在使用 addShutdownHook 时需要注意:如果有多个 hook,其执行是并发的,即无法确定 hook 的执行顺序

使用 abrupt shutdown 时,hook 不会执行Java 中有两类线程:用户线程 (User Thread) 和守护线程 (Daemon Thread),守护线程是指在程序运行时在后台提供一种通用服务的线程,比如垃圾回收线程,这种线程并不属于程序中不可或缺的部分。因此,当所有的非守护线程结束时,程序也就终止了,同时会杀死进程中的所有守护线程。反之,只要任何非守护线程还在运行,程序就不会终止。用户线程和守护线程几乎没有区别,只是在 JVM 关闭的时候,JVM 不会为守护线程做更多的工作,如:守护线程剩下的代码不会执行

Java 线程的生命周期

线程的 runnable 状态仅代表线程在 JVM 中开始执行,但从系统来看,该线程可能真的在运行,也可能在等待别的资源

参考

skeeey/note​github.com

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值