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

3.2 线程的状态与状态转换

调用Thread类对象实例的start方法来启动Java线程后,对应的底层操作系统线程不能马上得到CPU时间片来执行,需要等待操作系统的调度。所以为了便于跟踪Java线程的执行情况,Thread类定义了一系列的线程状态来表示当前线程的执行情况,同时线程的整个生命周期就是在这些状态之间转换。

在线上应用系统的运行过程当中,当发现服务器的CPU使用率特别高,怀疑出现死锁时,可以通过查看线程状态来定位当前正在执行的线程;当一个线程挂起迟迟没有执行完成时,可以查看该线程是在阻塞还是正在执行。

3.2.1 线程的状态

在3.1.1节介绍Thread类的使用时,我们分析了Thread类的几个核心方法,通过这些方法来对某个线程对象Thread的生命周期进行管理,从而对底层的操作系统线程的运行情况进行控制。为了方便对Java线程进行管理和对线程的运行情况进行跟踪,Java定义了6个线程状态来表示线程的当前运行情况,这6个状态具体在Thread类的内部枚举类State中定义,如下所示:

1.新建状态NEW

新建状态是指创建了Thread类对象实例,但是还没有调用start方法启动线程时的状态。此时Thread对象就是一个普通的Java对象,并不会执行任何操作。

2.可运行状态RUNNABLE

当调用了Thread类对象实例的start方法后,Thread类的线程状态由NEW转为RUNNABLE。如果操作系统的CPU此时是空闲的,则该Thread类线程对象对应的操作系统线程可以立即获取到CPU时间片并执行,此时对应的底层操作系统线程状态为运行中running。在执行时,如果之前在创建Thread类对象实例时,通过构造函数传入了一个Runnable接口的实现类对象,则会调用该Runnable接口的run方法。

如果此时CPU繁忙,不能马上分配CPU时间片资源来执行该线程,则该线程需要等待操作系统之后的调度来获得CPU时间片并执行。此时对应的底层操作系统线程的状态为就绪ready。

底层操作系统线程通常存在就绪ready和运行中running两种状态,而对应到JDK的线程定义Thread,则统一使用RUNNABLE这种状态来表示。在之后会介绍通过JDK的命令工具jstack来查看线程的堆栈情况,其中会有一个状态显示,如果某个线程显示为RUNNABLE,则通常表示该线程正在执行,即运行中running。

3.阻塞状态BLOCKED

当线程处于阻塞状态BLOCKED时,线程不能继续往下执行。这种情况主要出现在多个线程访问使用锁保护的共享资源时,线程阻塞等待获取锁资源,如等待进入使用了synchronized关键字加锁的方法或者代码块。只有当线程竞争到了该锁资源,才可以继续往下执行,即从阻塞BLOCKED状态转换为RUNNABLE状态,等待操作系统调度CPU获得时间片来执行。

4.等待状态WAITING

线程处于等待状态WAITING主要出现在与其他线程进行协作的场景中,具体为当前线程等待其他线程执行某种操作来通知和唤醒当前线程。这种场景对应的语义是当前线程由于缺少某些条件而无法继续往下执行,而这些条件需要其他线程来满足。所以当对应的条件满足时,其他线程会通知和唤醒当前线程,从而使得当前线程可以继续执行。一个典型的例子是生产者消费者模型。

在Thread类的方法设计中,主要是基于Object类的wait、notify和notifyAll方法实现的线程的生产者消费者模型。其中线程调用wait方法时,线程进入等待状态WAITING。

除此之外,当线程调用Thread类的join方法或者调用LockSupport的静态park方法时,线程也会进入等待状态WAITING。其中Thread类的join方法,通常用在主线程调用子线程Thread对象引用的join方法等待该子线程完成的过程。当子线程执行完成时,主线程被唤醒继续往下执行。调用LockSupport类的park方法导致的线程等待,需要在另一个线程调用对应的unpark方法来唤醒该线程。

当线程处于等待状态WAITING时,除不能继续往下执行外,还有个特性是如果当前线程持有锁,如在synchronized方法内或者synchronized同步代码块内部执行时,线程进入了等待状态,则会自动释放锁,这样其他线程可以竞争获取该锁。

5.超时等待状态TIMED_WAITING

超时等待状态TIMED_WAITING与等待状态WAITING功能类似,不同之处在于WAITING状态的等待不支持指定等待多长时间,没有超时机制。所以当条件不满足时,线程会无限等待下去。而处于超时等待状态TIMED_WAITING的线程,指定了最长等待的时间。如果超过这个时间,条件还没满足,还没有其他线程来唤醒当前线程,则当前线程会自动唤醒并继续往下执行。

使得线程处于TIMED_WAITING超时等待状态的方法调用主要包括:超时版本的Object类的wait方法、Thread类的join方法和LockSupport类的park方法。

6.终止状态TERMINATED

当Thread线程对象对应的线程执行完成时,线程进入终止状态TERMINATED。进入该状态之后,线程的状态不会再转换回其他状态,对应的Thread类对象也会被销毁回收。

3.2.2 线程的状态转换

上一小节介绍了Thread类的几个线程状态的含义,在线程的整个执行生命周期内,通过调用Thread类的相关方法来分别完成获取CPU资源并执行、暂停让出CPU资源或者执行完成等,这些过程对应到以上这些线程状态之间的转换。

Thread类的方法调用对应到线程状态的转换的映射关系,如图3.1所示。

图3.1 Thread类的方法调用与线程的状态转换

基于以上线程状态转换示意图,我们可以对Thread类的方法调用和线程状态的对应关系进行一个总结,线程状态和对应的转换过程具体如下。

(1)新建状态NEW:创建Thread类对象实例,线程进入新建状态NEW。

(2)可执行状态RUNNABLE:调用Thread类对象实例的start方法从新建状态NEW进入到可运行状态RUNNABLE。此时线程等待操作系统的调度和获取CPU时间片来执行。

(3)阻塞状态BLOCKED:当Thread类对象实例需要执行的方法使用synchronized加锁或者需要进入使用synchronized加锁的代码块时,如果此时synchronized对应的锁被其他线程占用了,则当前线程进入阻塞BLOCKED状态。当占用锁的线程释放锁,当前线程成功获取synchronized同步锁时,线程从阻塞状态BLOCKED进入可运行状态RUNNABLE。

(4)等待状态WAITING:当Thread类对象所执行的方法在内部调用了Object对象的wait方法,或者调用了Thread类对象自身的join方法,或者调用LockSupport类的park方法时,当前Thread线程对象进入等待状态WAITING。

之后当其他线程调用同一个Object对象的notify(当刚好通知到该线程时)或者notifyAll(通知所有等待的线程,故都会被通知到)方法,或者被等待的线程执行完成从join方法返回,或者在方法内部调用了LockSupport类的unpark(thread)方法时,当前Thread线程对象被唤醒,从等待状态WAITING进入可运行状态RUNNABLE。

(5)超时等待状态TIMED_WAITING:与进入等待状态WAITING类似,进入超时等待TIMED_WAITING也是使用以上方法,不过使用的是可指定超时时间的版本方法。具体为调用Object类对象的wait(long)方法,Thread类对象的join(long)方法,LockSupport类的parkNanos(this, long)和parkUtil方法,其中long为指定超时的时间。

(6)终止状态TERMINATED:当Thread类线程对象执行完对应的任务之后,则不再需要使用了,此时线程进入终止TERMINATED状态。当Thread类线程对象进入终止状态之后,不能再调用以上任何一个方法,如Thread类对象的start方法,否则会抛出java.lang.IllegalStateException异常。

3.2.3 JDK线程状态查看工具

在上一小节中主要介绍了Thread类线程对象的相关状态,而了解这些状态的含义和相关的转换规则,主要是为了在生产环境出现线程问题,如代码死循环时,我们能够根据线程的状态定位出产生问题的线程,然后进一步根据该线程所执行的代码找到问题的根源。

所以为了方便查看应用对应的JVM进程的线程情况,即线程的堆栈和线程状态情况,JDK提供了jstack这个命令来分析某个JVM进程的所有线程的堆栈和状态情况。

例如,在main方法中启动两个线程,分别取名为normalThread和deadLoopThread,其中normalThread是正常执行的线程,而deadLoopThread则是用于模拟出现死循环的线程。首先启动deadLoopThread线程,然后在主线程中调用deadLoopThread的join方法来等待该线程的执行完成,最后调用normalThread的start方法启动该线程。具体代码如下:

通过执行main方法的方式启动这个类,同时使用jps命令查看当前系统的Java进程,可以发现该演示类对应的进程ID为1418,如下所示:

然后使用jstack命令来查看该Java进程的线程堆栈情况,具体如下:

可以发现存在两个线程,分别为主线程main和死循环线程deapLoopThread。其中主线程main的线程状态为WAITING,而deadLoopThread的线程状态为RUNNABLE,表示deadLoopThread线程正在运行中,而main线程处于等待状态。

出现这个结果的原因是,先启动了deapLoopThread线程并且在主线程调用了deadLoopThread的join方法,等待deadLoopThread执行结束。由于deadLoopThread线程是死循环线程,一直在运行而不会结束,故线程状态为RUNNABLE;main主线程则一直在join方法调用处等待,故状态为WAITING。

由于一直阻塞在main线程的join方法调用处,故无法调用到normalThread线程对象的start方法,所以normalThread线程对象的状态一直保持为NEW。同时因为jstack命令不会打印输出状态为NEW的线程,所以在以上jstack命令的输出中不存在normalThread的打印。

关于jstack命令的更多使用方法,包含命令参数等,可以通过jstack-help来查看相关的使用指引和参数含义。 7WA+tYNkS3MUnoy8dpFgUFxCyn5H7T9q22LVLSIPYE0VKYVQD0SpFKpjLmK0L8+S

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