购买
下载掌阅APP,畅读海量书库
立即打开
畅读海量书库
扫码下载掌阅APP

2.2 线程的生命周期

Java线程的生命周期包含Java的六种基本状态,它们按一定的顺序和条件进行转换。Java线程的生命周期与人的生命周期相似,有一定的趋势,也有一些不可逆的过程。本节将介绍Java线程的生命周期。

2.2.1 线程的生命周期图谱

通过2.1节的学习,我们可以知道Java的线程有六种状态,这些状态之间按一定的规则联系,线程的状态会转变,这些状态间的转变有一定的规律和顺序。线程的活动就像人经历成长一样,有始有终,即有一个生命周期,所以可以将一个线程的活动总结成生命周期图谱,如图2.1所示。

图2.1中,圆角矩形一共有六个,每一个圆角矩形对应Java线程的一个状态。可以看出,线程的生命周期由NEW状态开始,由TERMINATED状态结束。其中,RUNNABLE状态中包含READY(就绪)和RUNNING(运行中)子状态,即READY和RUNNING都属于RUNNABLE状态。

图2.1 线程的生命周期图谱

当一个线程一开始调用start()方法时,线程就会处于RUNNABLE状态,但可能一开始未得到CPU资源,所以会处于READY子状态;一旦得到相关的CPU资源,就会进入RUNNING子状态。

注意,READY和RUNNING子状态并非Java线程所定义的六大状态之一,也无法通过相关方法得到READY和RUNNING的值,这里只是对Java的RUNNABLE状态加以细分说明。线程处于RUNNABLE状态时,有可能会与WAITING、TIMED_WAITING、BLOCKED状态相互转换。当线程完成所设定的任务后,最终会到达TERMINATED状态,完成自己的使命,线程的生命周期结束。

2.2.2 线程的生命周期图谱分析一:新建和可运行中的就绪

用第1章介绍的一种方法创建一个线程类,并且使用NEW关键字创建一个线程对象实例,就是NEW(新建)状态。当需要启动线程时,调用start()方法,就会进入RUNNABLE状态。

一般地,RUNNABLE状态实际上包含预备中的READY(就绪)状态,以及真正运行起来的RUNNING(运行中)状态。READY状态与RUNNING状态之间的转换与系统的调度有关,如CPU资源的获取等,具体可以通过第3章的学习加深认识。如果READY状态下的线程获得了足够的资源,就会马上转为RUNNING状态;同时,如果一个正处于RUNNING状态的线程主动做出让步,即使用了yieId()方法把CPU等资源让出来,则此时该线程就会重新处于READY状态,如图2.2所示。

图2.2 线程生命周期中的新建、就绪与运行中

从图2.2中可以看出,圆角矩形中的NEW和RUNNABLE代表Java线程六种状态的前两种,而椭圆形中的READY和RUNNING为RUNNABLE的子状态。Java中没有把READY和RUNNING直接作为线程的枚举类型状态,是因为当今的计算机处理能力与几十年前相比已经大幅提高,系统调度中的CPU分片的时间段已经非常小,几乎是毫秒级别的。

这对于Java的虚拟机在启动多线程的情况下,在这么短的时间内说明一个线程到底是READY还是RUNNING的意义其实已经不大了,因此Java虚拟机把这样的状态统一为RUNNABLE状态,告诉操作系统线程是可运行的就足够了。至于到底是READY还是RUNNING,这属于系统调度的工作范围,而且是超短时间内完成的,我们几乎察觉不到。更多多线程的调度知识将在第3章中讲解。

2.2.3 线程的生命周期图谱分析二:可运行和阻塞

在多线程中,某个线程需要对一个数据进行操作时,为了确保该数据的操作正确性,需要设置同步来保护数据。可以使用synchronized关键字对一段代码设置同步块或设置一个同步方法,这样通过synchronized关键字修饰的同步块或同步方法就会加入一个锁,以锁定数据确保同步。只有该线程完成操作后,其他线程才能再次对该数据进行操作。在该过程中,当一个线程设置了同步块或同步方法,而其他线程进入了同步块或同步方法时,就会发生线程的阻塞,如图2.3所示。

图2.3 线程生命周期中的运行与阻塞

图2.3中,圆角矩形中的RUNNABLE和BLOCKED代表Java线程六种状态的其中两种,也展示出了RUNNABLE状态与BLOCKED状态之间的转换是如何发生的。可见,由RUNNABLE到BLOCKED状态的转换与多线程同步关键字synchronized定义的块/方法有关。多线程的同步是一个难点,关于多线程的同步及synchronized关键字的详细内容将在第7章中重点介绍。同样,这里也用一个简单的示例先简单说明。

假设有一个游戏欢乐城,里面有许多电子投币式的娱乐设备。有5个孩子,每个人手上都有300个1元硬币,可以随时向娱乐设备中投掷硬币。每投掷一个硬币,游戏欢乐城的商家今日的收入就会增加1元。尝试用多线程来模拟这个过程,参考代码如下:

如果这5个孩子都用完了自己的300个硬币,那么商家增加的收入是1500元。运行代码,看是否真的能够计算出这个结果。一般地,大部分运行结果会和下面的参考结果相似:

但当我们运行多次时,偶尔会出现非1500的情况。例如:

这些数值一般小于1500,如果把循环中的300改为3000,甚至30000,结果会更为明显。这是因为上面的代码中,在多线程的情况下对同一个值进行操作时,其中的一个线程取了另外一个线程还没有来得及更新的数值,然后在此基础上进行更新,这就导致了数据错漏的情况,是一种线性不安全的表现。这时,可以加入多线程的同步机制来解决这个问题。修改后的同步代码如下:

这个带有同步功能的代码,只是加入了synchronized关键字并修改了下面这几行代码:

也就是说,当多线程中的其中一个线程对amount变量进行+1操作时,其他线程如果也进入该同步块区域,就会进入阻塞状态,直到对amount变量进行+1的操作完成了该步骤后,其他线程中的一个才能进行下一步操作。

2.2.4 线程的生命周期图谱分析三:等待与恢复

因为一些业务的需求,在多线程的项目中,部分线程经常需要进入等待。在Java的线程中,与等待相关的状态占据了六种Java状态中的两种:WAITING,即等待状态;TIMED_WAITING,即调校时间的等待状态。

一般地,如果代码中出现了wait()方法或join()方法,则线程会进入WAITING状态;如果代码中出现了sIeep(Iong型毫秒数)方法、wait(Iong型毫秒数)方法、join(Iong型毫秒数)方法,则线程会进入TIMED_WAITING状态。它们的重新唤醒恢复都是通过notify()及notifyAII()方法完成的。线程生命周期中的等待与调校时间的等待,如图2.4所示。

图2.4 线程生命周期中的等待与调校时间的等待

图2.4中,圆角矩形代表Java的线程状态。其中,WAITING及TIMED_WAITING状态分别可以与RUNNABLE状态相互转换。而WAITING状态与TIMED_WAITING状态的进入,从表面上来看,最大的不同在于进入TIMED_WAITING状态的触发方法一般都带有一个长整型(Iong)的颗粒度为毫秒数的时间参数,以代表需要调校的定时等待时间为这么多毫秒数。wait()、wait(XXX)、join()、join(XXX)、notify()、notifyAII()等方法将在第3章中详细介绍。

2.2.5 线程的终止与关闭

一个线程的终止,可以划分为内在原因和外在原因两类。

内在原因:当一个线程完成了自己的使命,处理完所有的业务逻辑后,会自动进入TERMINATED状态,该线程的生命周期结束;因为线程的内部逻辑处理中出现了未能控制好的异常,导致报错,线程异常终止。

外在原因:操作人员手动杀死线程或进程;人工调用了关闭线程的方法,如用interrupt()方法设置了中断标志,然后在捕获InterruptedException异常的代码中处理线程的安全关闭。

线程一旦到了TERMINATED状态,也就意味着线程的生命周期结束,则该线程的所有方法都应该停止再次调用,包括start()方法,不能重新恢复该线程的生命周期。到这里,我们结合之前的内容,对图2.1的线程生命周期图谱进行扩充,可以得到更详细的线程生命周期图谱,如图2.5所示。

图2.5 线程的生命周期图谱详细版

以上就是整个Java线程的生命周期完整图谱。理解线程的生命周期,将有助于了解在多线程环境下的各个线程的协同处理,以及某些业务需求下的多线程的高并发情景。线程的安全关闭将在第5章中详细讲解。 ufSpkACOncZ3A2QDH+5ANEVa6lKtKXhpHF17qCvyXbe9npkctK0+L4iZV/FsAHo8

点击中间区域
呼出菜单
上一章
目录
下一章
×