Java 多线程系列 3:Java 线程的一生
Java 线程和人一样,也要经历自己的生老病死。
首先我们要理解操作系统的线程的生命周期。因为 Java 线程是一个逻辑上的概念,真正执行代码的是操作系统的线程。这里以 Linux 操作系统为例。
在 Linux 操作系统中,线程实际上被实现为一个轻量级的进程,因此其生命周期和进程很类似。一个线程被创建后进入就绪状态,此时意味着这个线程可以被 CPU 调度执行。然后这个线程等到了 CPU 的时间分片,进行运行状态,时间片用完后这个线程被切换出去进入就绪状态,如此反复循环,直到这个线程正常执行完成或者异常退出。在执行过程中,线程可能会执行一些 IO 操作,这回导致线程被阻塞进入阻塞状态,直到 IO 操作完成,线程被唤醒重新进入就绪状态。
Java 的线程只是对操作系统线程的轻量级封装,但对操作系统线程的阻塞状态做了更细化的分类,同时把操作系统线程的就绪和运行状态合并。这样做的目的是为了更好契合 Java 语言本身对于使用线程编程的理念。
以下是 Java 语言中线程的 6 种状态和操作系统状态的映射关系:
Java 的线程 Linux 线程
新建(New) --(创建)
可运行(Runnable) -- (就绪、运行)
阻塞(Blocked) -- (阻塞)
无线等待(Waiting) -- (阻塞)
有限等待(Timed_Waiting) -- (阻塞)
终止(Terminated)-- (终止)
线程的出生
当我们使用如下语句创建一个线程时,只是在 Java 语言层面定义了一个线程,线程并没有真正的诞生。
只有当我们调用 thread.start 方法时,才会真正在操作系统层面创建一个线程。这意味着线程出生了!
线程会老去吗?
和人不一样,线程一出生就可以干活,而且不知疲倦(这点比人强多了!),只要资源(CPU、内存)充足,它可以长生不老。但线程会“生病”。
线程生病了
线程典型的病态有:僵尸线程、死锁、饥饿。
1. 僵尸线程
所谓僵尸线程就是线程已经执行完成,但是在线程列表中依然可以看到这个线程。这个线程不会被重新调度,但依然会占有一部分资源,导致“尸位素餐”的现象。如果僵尸线程过多,会严重影响系统性能。发生僵尸线程的主要原因是在主线程和子线程协同的场景中,主线程没有使用调用子线程的 join 方法来等待子线程结束,导致子线程还没开始执行,主线程就结束了。这时子线程执行完后就成了僵尸进程。因为子线程的生命周期没有被正确管理。因此一个良好的编程习惯是:当主线程要启动一个子线程时,一定记得调用子线程的 join 方法。
在 linux 中,可以使用如下命令查找和杀死僵尸线程:
2. 死锁
线程死锁是指两个或更多的线程在执行过程中,因争夺两个以上的共享资源而造成的一种僵局。如果没有对共享资源的竞争,就不会产生死锁。比如:有两个共享资源 X 和 Y,线程 A 和 B 要运行都需要获得 X 和 Y 的锁。如果 A 线程先获得 X 的锁然后等待 Y 的锁,而同时线程 B 先获得 Y 的锁而等待 X 的锁,那么这两个线程将处于死锁的状态,永远相互等待下去。
要破解线程死锁,有多种方式,比如:顺序加锁法、一次加锁法、不使用竞争资源等。后面再专门写文章介绍。
3. 饿死
线程饿死是指一个线程因为某种原因,无法获得执行所需的资源而处于一直等待执行的状态。导致线程饿死的原因有很多,比如高优先级的线程占用了绝大多数的 CPU 时间,导致低优先级的线程发生饥饿,只能偶尔被调度执行。这在线程池等调度系统中很常见,破解的办法就是使用公平调度的策略。
还有一些原因也会导致线程饿死,例如某个线程进入 synchronized 模块后,一直执行不释放锁,导致其他线程一致等待而得不到执行。这种情况是由于程序设计不合理导致。使用 synchronized 关键字加锁的代码块要确保能够快速执行完成,或者可以使用 JDK 的可重入锁来对资源进行加锁。
优雅的告别
正常情况下,线程执行完就自动进入终止状态了。如果执行过程发生异常,也会导致线程终止。但有时候我们需要强制杀死一个线程,比如线程在等待一个很长的 IO 操作,我们等待不下去了(这在 UI 交互场景中很常见),希望人工终止线程执行。在程序设计中,我们需要用一种优雅的方式来终止线程。
在 Thread 类中有一个方法:stop(),这个方法会直接杀死线程,不给线程任何“善后”的机会。如果线程持有某个锁然后被调用 stop 方法杀死,那么这个锁不会被释放,导致别的线程也无法获得这个锁。因为 stop 方法简单粗暴过于危险,因此这种方式现在不再推荐使用。当然,如果你很确定你的程序调用 stop 不会有任何危险,也是可以用的。
优雅的终止线程的方法是使用 interrupt 方法。相比于 stop 方法,interrupt 方法只是通知线程:你该退出执行了。但线程可以选择响应,也可以选择无视这个通知(取决于编写程序的人)。线程收到 interrupt 通知时有可能处于阻塞状态(waiting,timed_waiting),这时 Java 会触发一个 InterruptedException,线程捕获这个异常后可以进行退出的逻辑。
如果线程一直处于 running 的状态,则需要线程自己主动检测自己是否被 interrupt 了。线程可以调用 isInterrupted()方法检测自己的状态,如果为 true,则执行退出的逻辑。
最后
线程的一生除了干活,也要睡觉(sleep),也需要和别的线程协同,等待帮助(wait),下节再介绍。
版权声明: 本文为 InfoQ 作者【BigBang!】的原创文章。
原文链接:【http://xie.infoq.cn/article/242a10f6ff4045c3725dba988】。文章转载请联系作者。
评论