Loading...
墨滴

乘风

2021/04/09  阅读:48  主题:全栈蓝

详解java线程状态及转换

前言

线程也有自己的“生老病死”,专业的说法就是生命周期,而掌握线程的生命周期,能帮我们快速分析和定位线程相关的一些问题。比如说,当我们打印线程的堆栈,发现某线程一直处于BOCKED状态,我们就可以以此推测是不是锁没有释放导致的,然后根据堆栈信息定位到具体的方法,进一步排查问题。

通用线程生命周期模型

在讲java的线程生命周期之前,需要先了解一下通用的线程生命周期模型,因为各种语言包括中线程的本质其实就是操作系统的线程,只是不同语言进行了不同程度的封装。通用的生命周期模型主要包含了五个状态节点,简称为"五态模型",其状态图如下:

image-20210403204854421
image-20210403204854421
  • 初始状态:初始状态是指在编程语言层面已经将线程创建好了,不过此时在操作系统层面,线程还没有真正创建,这时的线程还不具备执行的能力。

  • 可运行状态:此时的线程已经具备执行的能力了,处于调度队列中,就差被选中分配到cpu上执行。

  • 运行状态:线程被调度器选中,拿到时间片,到cpu上执行。

  • 休眠状态:线程阻塞或休眠,会让出cpu,处于休眠状态。当线程解除休眠状态后,会再次进入可运行状态,接受线程调度器调度。

  • 终止状态:线程被异常中断或结束运行,进入终结状态。

java线程生命周期

状态说明

首先,我们看下,jdk的Thread类对线程状态的定义:

public enum State {
        //初始状态
        NEW,
    
      //运行状态
        RUNNABLE,

      //阻塞状态
        BLOCKED,
  
      //等待状态
        WAITING,

      //可超时等待状态
        TIMED_WAITING,

      //终结状态
        TERMINATED;
}

可以看出这个枚举类,一共定义了6个状态,看起来还挺复杂的,不要慌,听我讲完后,你会发现其实挺简单的。对于这种状态流程类的事物,有一种非常好的处理技巧,就是先找主干,再看分支,这个对于理解java生命周期也同样适用。

主干流程

首先,我们看下主干流程。假设有一个线程,从它创建到终止,执行过程中没有遇到任何阻塞或等待,非常顺利,那么他的生命周期图就是下面这样直溜溜的: image-20210409213504092 可以看到,主干流程中就三个状态:初始、运行、终止,详细讲解下:

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状态后,就成这样了: image-20210409213441863

WAITING(等待)

waiting用于表示线程处于休眠等待状态,有三种情况会使线程从RUNNABLE转变为WAITING状态

  1. 调用Thread.join();
  2. 调用LockSupport.park()/park(Object);
  3. 调用Object.wait(); 对应的,满足以下三种情况后,线程会从WAITING状态,从新恢复运行变成RUNNABLE状态:
  4. 等待的线程执行完毕,在Thread.join()等待的线程恢复运行;
  5. 调用LockSupport.unpark(Thread) 唤醒在Object.wait()方法等待的线程;
  6. 调用Object.notify()/notifyAll() 唤醒在LockSupport.park()/park(Object)方法等待的线程;

除此之外通过调用线程的interrupt()方法,也可以直接将处于WAITING的线程唤醒,转为运行状态。 以下是加上WAITING状态后的状态图: image-20210409213420348

TIMED_WAITING(可超时等待)

TIMED_WAITING状态,也就是可超时等待状态,和等待状态不同的地方在于,加了个超时时间,也就是说明线程不会一直等,如果等待时间超过了设置的超时时间,会自动回到运行状态。 以下api会导致线程进入可超时等待状态:

  1. Thread.join(long)
  2. Object.wait(long)
  3. LockSupport.parkNanos(long)/parkUntil(long)
  4. Thread.sleep(long) 相应的出现以下情况会使线程恢复到运行状态:
  5. 等待的线程执行完毕,或者等待超出设置的时间,使阻塞在join(long)方法的线程恢复运行;
  6. 调用Object.notify()/notifyAll(),或者等待超出设置的时间,使阻塞在wait(long)方法的线程恢复运行;
  7. 调用LockSupport.unpark(Thread),或者等待超出设置的时间,使阻塞在LockSupport.parkNanos(long)/parkUntil(long)的线程恢复运行;
  8. 线程休眠时间超出设置的时间,使阻塞在Thread.sleep(long)的线程恢复运行 另外,处于TIMED_WAITING状态线程,如果调用其interrupt()方法,可以使其恢复运行状态。最终,我们的生命周期图变成这样了 image-20210409213338461

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命令来验证一下:

  1. 通过jps列出所有java进程,可以看到进程JstackTest的pid为72146 image-20210403204653951
  2. 执行jstack ,打印堆栈信息。可以看到,结果是符合我们猜测的。 image-20210403204736675

总结

本文主要讲解了通用线程生命周期和java线程生命周期,两者的主要差别在于java线程生命周期在通用线程生命周期基础上进行了简化和扩展,简化是指将可运行状态和运行状态合并为运行状态,扩展是指将休眠状态细分为阻塞状态,等待状态、超时等待状态,同时还详细讲解了java线程生命周期各种状态之间是如何转换的,最后介绍了如何通过jstack命令打印线程堆栈,查看线程状态。

乘风

2021/04/09  阅读:48  主题:全栈蓝

作者介绍

乘风

公众号:java侠