通用线程生命周期模型线程也有自己的“生老病死”,专业的说法就是生命周期,而掌握线程的生命周期,能帮我们快速分析和定位线程相关的一些问题。比如说,当我们打印线程的堆栈,发现某线程一直处于BOCKED状态,我们就可以以此推测是不是锁没有释放导致的,然后根据堆栈信息定位到具体的方法,进一步排查问题。
在讲java的线程生命周期之前,需要先了解一下通用的线程生命周期模型,因为各种语言包括中线程的本质其实就是操作系统的线程,只是不同语言进行了不同程度的封装。通用的生命周期模型主要包含了五个状态节点,简称为"五态模型",其状态图如下:
<u>初始状态</u>:初始状态是指在编程语言层面已经将线程创建好了,不过此时在操作系统层面,线程还没有真正创建,这时的线程还不具备执行的能力。
<u>可运行状态</u>:此时的线程已经具备执行的能力了,处于调度队列中,就差被选中分配到cpu上执行。
<u>运行状态</u>:线程被调度器选中,拿到时间片,到cpu上执行。
<u>休眠状态</u>:线程阻塞或休眠,会让出cpu,处于休眠状态。当线程解除休眠状态后,会再次进入可运行状态,接受线程调度器调度。
<u>终止状态</u>:线程被异常中断或结束运行,进入终结状态。
状态说明
首先,我们看下,jdk的Thread类对线程状态的定义:
public enum State { //初始状态 NEW, //运行状态 RUNNABLE, //阻塞状态 BLOCKED, //等待状态 WAITING, //可超时等待状态 TIMED_WAITING, //终结状态 TERMINATED; }
可以看出这个枚举类,一共定义了6个状态,看起来还挺复杂的,不要慌,听我讲完后,你会发现其实挺简单的。对于这种状态流程类的事物,有一种非常好的处理技巧,就是先找主干,再看分支,这个对于理解java生命周期也同样适用。
主干流程
首先,我们看下主干流程。假设有一个线程,从它创建到终止,执行过程中没有遇到任何阻塞或等待,非常顺利,那么他的生命周期图就是下面这样直溜溜的:
<img src="https://gitee.com/thomasChant/drawing-bed/raw/master/image-20210401220546262.png" alt="image-20210401220546262" style="zoom:100%;" />
可以看到,主干流程中就三个状态:初始、运行、终止,详细讲解下:
NEW(创建)
调用线程构造方法如new Thread(),创建一个线程,此时线程还不具备执行的能力,只是在语言层面已经创建好了。我们可以调用线程的getState()方法获取到线程状态。
RUNNABLE(运行)
调用线程的start()方法启动线程,线程状态变为RUNNABLE(运行状态),此时线程才真正在操作系统层面被创建,并加入调度队列中,一旦分配到时间片后就会执行。至于何时分配到时间片真正开始执行,在java层面是不关心的,这些都是操作系统在背后暗箱操作,线程无论是正在执行还是等待执行,对于java来讲都一并视为运行状态。
TERMINATED(终止)
当线程执行完毕,或者被stop()(废弃方法,不建议使用)了,就会进入TERMINATED状态。
分支流程
上面讲了个幸运的线程,从创建到结束,顺顺利利,没有什么波折,但是并非所有线程都能这么幸运的。前面在讲通用线程模型时,我们看到其实还有一种状态叫休眠状态,用来表示线程完全让出cpu暂停执行的状态,对于java线程来讲,也有休眠状态,只不过将其细分成了三种状态:
BLOCKED(阻塞),
WAITING(等待),
TIMED_WAITING(可超时等待)
接下来我们具体分析一下这三种状态。
BLOCKED(阻塞)
当线程处于BLOCKED状态,说明线程当下处于监视器锁(即synchronzied关键字修饰的锁)的等待队列中。
需要特别注意的是,此处的阻塞和调用阻塞api,等待返回的阻塞是不一样的,线程因为调用阻塞api而处于“阻塞状态”,比如nio中的select()方法,此时线程只是在操作系统层面处于休眠状态,但是在jvm层面,仍将该线程视为运行。
最后需要强调一下,该状态仅针对监视器锁,线程等待显式锁(基于AQS框架的锁),并不会处于BLOCKED状态,而是下文讲的WAITING或TIMED_WAITING状态。因为显式锁是基于LockSupport的park方法实现(后面会出一篇文章详细讲解LockSupport),而park方法会使线程进入WAITING或TIMED_WAITING状态而非BLOCKED。
我们在java生命周期图上加上BLOCKED状态后,就成这样了:
<img src="https://gitee.com/thomasChant/drawing-bed/raw/master/image-20210403201150287.png" alt="image-20210403201150287" style="zoom:100%;" />
WAITING(等待)
waiting用于表示线程处于休眠等待状态,有三种情况会使线程从RUNNABLE转变为WAITING状态
- 调用Thread.join();
- 调用LockSupport.park()/park(Object);
- 调用Object.wait();
对应的,满足以下三种情况后,线程会从WAITING状态,从新恢复运行变成RUNNABLE状态:
- 等待的线程执行完毕,在Thread.join()等待的线程恢复运行;
- 调用LockSupport.unpark(Thread) 唤醒在Object.wait()方法等待的线程;
- 调用Object.notify()/notifyAll() 唤醒在LockSupport.park()/park(Object)方法等待的线程;
除此之外通过调用线程的interrupt()方法,也可以直接将处于WAITING的线程唤醒,转为运行状态。
以下是加上WAITING状态后的状态图:
<img src="https://gitee.com/thomasChant/drawing-bed/raw/master/image-20210403201005664.png" alt="image-20210403201005664" style="zoom: 100%;" />
TIMED_WAITING(可超时等待)
TIMED_WAITING状态,也就是可超时等待状态,和等待状态不同的地方在于,加了个超时时间,也就是说明线程不会一直等,如果等待时间超过了设置的超时时间,会自动回到运行状态。
以下api会导致线程进入可超时等待状态:
- Thread.join(long)
- Object.wait(long)
- LockSupport.parkNanos(long)/parkUntil(long)
- Thread.sleep(long)
相应的出现以下情况会使线程恢复到运行状态:
- 等待的线程执行完毕,或者等待超出设置的时间,使阻塞在join(long)方法的线程恢复运行;
- 调用Object.notify()/notifyAll(),或者等待超出设置的时间,使阻塞在wait(long)方法的线程恢复运行;
- 调用LockSupport.unpark(Thread),或者等待超出设置的时间,使阻塞在LockSupport.parkNanos(long)/parkUntil(long)的线程恢复运行;
- 线程休眠时间超出设置的时间,使阻塞在Thread.sleep(long)的线程恢复运行
另外,处于TIMED_WAITING状态线程,如果调用其interrupt()方法,可以使其恢复运行状态。最终,我们的生命周期图变成这样了
<img src="https://gitee.com/thomasChant/drawing-bed/raw/master/image-20210403201108311.png" alt="image-20210403201108311" style="zoom:100%;" />
jstack工具前面讲过掌握了线程的生命周期,可以帮助我们更快速的定位和排查多线程相关的bug,具体应该怎么做呢?介绍一种简单有效的办法,那就是jstack命令,这是jdk自带的一个工具,可以打印java进程中所有的线程的状态及堆栈,帮助我们快速定位问题。这里结合一个简单的案例,讲解一下jstack的使用方式。代码如下:
public class JstackTest { private static Object lock = new Object(); public static void main(String[] args) { Runnable runnable = () -> { synchronized (lock) { Sleep.seconds(100); } }; Thread t1 = new Thread(runnable,"t1"); Thread t2 = new Thread(runnable,"t2"); t1.start(); t2.start(); } }
在这段代码中,创建了两个线程t1和t2,两个线程启动后,会有一个先拿到锁,睡眠100s,处于TIMED_WAITING状态,另一个则会阻塞直到先拿到锁的线程释放锁才执行,处于BLOCKED状态。我们使用jstack命令来验证一下:
通过jps列出所有java进程,可以看到进程JstackTest的pid为72146
执行jstack ,打印堆栈信息。可以看到,结果是符合我们猜测的。
本文主要讲解了通用线程生命周期和java线程生命周期,两者的主要差别在于java线程生命周期在通用线程生命周期基础上进行了简化和扩展,简化是指将可运行状态和运行状态合并为运行状态,扩展是指将休眠状态细分为阻塞状态,等待状态、超时等待状态,同时还详细讲解了java线程生命周期各种状态之间是如何转换的,最后介绍了如何通过jstack命令打印线程堆栈,查看线程状态。