Java线程生命周期与状态切换

前提

最近有点懒散,没什么比较有深度的产出。恰好想从新研读一下JUC线程池的源码实现,在此以前先深刻了解一下Java中的线程实现,包括线程的生命周期、状态切换以及线程的上下文切换等等。编写本文的时候,使用的JDK版本是11。java

Java线程的实现

JDK1.2以后,Java线程模型已经肯定了基于操做系统原生线程模型实现。所以,目前或者从此的JDK版本中,操做系统支持怎么样的线程模型,在很大程度上决定了Java虚拟机的线程如何映射,这一点在不一样的平台上没有办法达成一致,虚拟机规范中也未限定Java线程须要使用哪一种线程模型来实现。线程模型只对线程的并发规模和操做成本产生影响,对于Java程序来讲,这些差别是透明的。git

对应Oracle Sun JDK或者说Oracle Sun JVM而言,它的Windows版本和Linux版本都是使用一对一的线程模型实现的(以下图所示)。github

也就是一条Java线程就映射到一条轻量级进程(Light Weight Process)中,而一条轻量级线程又映射到一条内核线程(Kernel-Level Thread)。咱们平时所说的线程,每每就是指轻量级进程(或者说咱们平时新建的java.lang.Thread就是轻量级进程实例)。前面推算这个线程映射关系,能够知道,咱们在应用程序中建立或者操做的java.lang.Thread实例最终会映射到系统的内核线程,若是咱们恶意或者实验性无限建立java.lang.Thread实例,最终会影响系统的正常运行甚至致使系统崩溃(能够在Windows开发环境中作实验,确保内存足够的状况下使用死循环建立和运行java.lang.Thread实例)。shell

线程调度方式包括两种,协同式线程调度和抢占式线程调度。编程

线程调度方式 描述 劣势 优点
协同式线程调度 线程的执行时间由线程自己控制,执行完毕后主动通知操做系统切换到另外一个线程上 某个线程若是不让出CPU执行时间可能会致使整个系统崩溃 实现简单,没有线程同步的问题
抢占式线程调度 每一个线程由操做系统来分配执行时间,线程的切换不禁线程自身决定 实现相对复杂,操做系统须要控制线程同步和切换 不会出现一个线程阻塞致使系统崩溃的问题

Java线程最终会映射为系统内核原生线程,因此Java线程调度最终取决于系操做系统,而目前主流的操做系统内核线程调度基本都是使用抢占式线程调度。也就是能够死记硬背一下:Java线程是使用抢占式线程调度方式进行线程调度的缓存

不少操做系统都提供线程优先级的概念,可是因为平台特性的问题,Java中的线程优先级和不一样平台中系统线程优先级并不匹配,因此Java线程优先级能够仅仅理解为“建议优先级”,通俗来讲就是java.lang.Thread#setPriority(int newPriority)并不必定生效,有可能Java线程的优先级会被系统自行改变bash

Java线程的状态切换

Java线程的状态能够从java.lang.Thread的内部枚举类java.lang.Thread$State得知:多线程

public enum State {
      
    NEW,

    RUNNABLE,

    BLOCKED,

    WAITING,

    TIMED_WAITING,

    TERMINATED;
}
复制代码

这些状态的描述总结成图以下:并发

线程状态之间关系切换图以下:工具

下面经过API注释和一些简单的代码例子分析一下Java线程的状态含义和状态切换。

NEW状态

API注释

/** * Thread state for a thread which has not yet started. * */
NEW,
复制代码

线程实例还没有启动时候的线程状态。

一个刚建立而还没有启动(还没有调用Thread#start()方法)的Java线程实例的就是出于NEW状态。

public class ThreadState {

    public static void main(String[] args) throws Exception {
        Thread thread = new Thread();
        System.out.println(thread.getState());
    }
}

// 输出结果
NEW
复制代码

RUNNABLE状态

API注释

/** * Thread state for a runnable thread. A thread in the runnable * state is executing in the Java virtual machine but it may * be waiting for other resources from the operating system * such as processor. */
RUNNABLE,
复制代码

可运行状态下线程的线程状态。可运行状态下的线程在Java虚拟机中执行,但它可能执行等待操做系统的其余资源,例如处理器。

当Java线程实例调用了Thread#start()以后,就会进入RUNNABLE状态。RUNNABLE状态能够认为包含两个子状态:READYRUNNING

  • READY:该状态的线程能够被线程调度器进行调度使之更变为RUNNING状态。
  • RUNNING:该状态表示线程正在运行,线程对象的run()方法中的代码所对应的的指令正在被CPU执行。

当Java线程实例Thread#yield()方法被调用时或者因为线程调度器的调度,线程实例的状态有可能由RUNNING转变为READY,可是从线程状态Thread#getState()获取到的状态依然是RUNNABLE。例如:

public class ThreadState1 {

    public static void main(String[] args) throws Exception {
        Thread thread = new Thread(()-> {
            while (true){
                Thread.yield();
            }
        });
        thread.start();
        Thread.sleep(2000);
        System.out.println(thread.getState());
    }
}
// 输出结果
RUNNABLE
复制代码

WAITING状态

API注释

/** * Thread state for a waiting thread. * A thread is in the waiting state due to calling one of the * following methods: * <ul> * <li>{@link Object#wait() Object.wait} with no timeout</li> * <li>{@link #join() Thread.join} with no timeout</li> * <li>{@link LockSupport#park() LockSupport.park}</li> * </ul> * * <p>A thread in the waiting state is waiting for another thread to * perform a particular action. * * For example, a thread that has called {@code Object.wait()} * on an object is waiting for another thread to call * {@code Object.notify()} or {@code Object.notifyAll()} on * that object. A thread that has called {@code Thread.join()} * is waiting for a specified thread to terminate. */
    WAITING,
复制代码

等待中线程的状态。一个线程进入等待状态是因为调用了下面方法之一:

不带超时的Object#wait()

不带超时的Thread#join()

LockSupport.park()

一个处于等待状态的线程老是在等待另外一个线程进行一些特殊的处理。

例如:一个线程调用了Object#wait(),那么它在等待另外一个线程调用对象上的Object#notify()或者Object#notifyAll();一个线程调用了Thread#join(),那么它在等待另外一个线程终结。

WAITING无限期的等待状态,这种状态下的线程不会被分配CPU执行时间。当一个线程执行了某些方法以后就会进入无限期等待状态,直到被显式唤醒,被唤醒后,线程状态由WAITING更变为RUNNABLE而后继续执行。

RUNNABLE转换为WAITING的方法(无限期等待) WAITING转换为RUNNABLE的方法(唤醒)
Object#wait() Object#notify() | Object#notifyAll()
Thread#join() -
LockSupport.park() LockSupport.unpark(thread)

其中Thread#join()方法相对比较特殊,它会阻塞线程实例直到线程实例执行完毕,能够观察它的源码以下:

public final void join() throws InterruptedException {
    join(0);
}

public final synchronized void join(long millis)throws InterruptedException {
    long base = System.currentTimeMillis();
    long now = 0;

    if (millis < 0) {
        throw new IllegalArgumentException("timeout value is negative");
    }

    if (millis == 0) {
        while (isAlive()) {
            wait(0);
        }
    } else {
        while (isAlive()) {
            long delay = millis - now;
            if (delay <= 0) {
                break;
            }
            wait(delay);
            now = System.currentTimeMillis() - base;
        }
    }
}
复制代码

可见Thread#join()是在线程实例存活的时候老是调用Object#wait()方法,也就是必须在线程执行完毕isAlive()为false(意味着线程生命周期已经终结)的时候才会解除阻塞。

基于WAITING状态举个例子:

public class ThreadState3 {

    public static void main(String[] args) throws Exception {
        Thread thread = new Thread(()-> {
            LockSupport.park();
            while (true){
                Thread.yield();
            }
        });
        thread.start();
        Thread.sleep(50);
        System.out.println(thread.getState());
        LockSupport.unpark(thread);
        Thread.sleep(50);
        System.out.println(thread.getState());
    }
}
// 输出结果
WAITING
RUNNABLE
复制代码

TIMED WAITING状态

API注释

/** * Thread state for a waiting thread with a specified waiting time. * A thread is in the timed waiting state due to calling one of * the following methods with a specified positive waiting time: * <ul> * <li>{@link #sleep Thread.sleep}</li> * <li>{@link Object#wait(long) Object.wait} with timeout</li> * <li>{@link #join(long) Thread.join} with timeout</li> * <li>{@link LockSupport#parkNanos LockSupport.parkNanos}</li> * <li>{@link LockSupport#parkUntil LockSupport.parkUntil}</li> * </ul> */
TIMED_WAITING,
复制代码

定义了具体等待时间的等待中线程的状态。一个线程进入该状态是因为指定了具体的超时期限调用了下面方法之一:

Thread.sleep()

带超时的Object#wait()

带超时的Thread#join()

LockSupport.parkNanos()

LockSupport.parkUntil()

TIMED WAITING就是有限期等待状态,它和WAITING有点类似,这种状态下的线程不会被分配CPU执行时间,不过这种状态下的线程不须要被显式唤醒,只须要等待超时限期到达就会被VM唤醒,有点相似于现实生活中的闹钟。

RUNNABLE转换为TIMED WAITING的方法(有限期等待) TIMED WAITING转换为RUNNABLE的方法(超时解除等待)
Object#wait(timeout) -
Thread#sleep(timeout) -
Thread#join(timeout) -
LockSupport.parkNanos(timeout) -
LockSupport.parkUntil(timeout) -

举个例子:

public class ThreadState4 {

    public static void main(String[] args) throws Exception {
        Thread thread = new Thread(()-> {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                //ignore
            }
        });
        thread.start();
        Thread.sleep(50);
        System.out.println(thread.getState());
        Thread.sleep(1000);
        System.out.println(thread.getState());
    }
}
// 输出结果
TIMED_WAITING
TERMINATED
复制代码

BLOCKED状态

API注释

/** * Thread state for a thread blocked waiting for a monitor lock. * A thread in the blocked state is waiting for a monitor lock * to enter a synchronized block/method or * reenter a synchronized block/method after calling * {@link Object#wait() Object.wait}. */
BLOCKED,
复制代码

此状态表示一个线程正在阻塞等待获取一个监视器锁。若是线程处于阻塞状态,说明线程等待进入同步代码块或者同步方法的监视器锁或者在调用了Object#wait()以后重入同步代码块或者同步方法。

BLOCKED状态也就是阻塞状态,该状态下的线程不会被分配CPU执行时间。线程的状态为BLOCKED的时候有两种可能的状况:

A thread in the blocked state is waiting for a monitor lock to enter a synchronized block/method

  1. 线程正在等待一个监视器锁,只有获取监视器锁以后才能进入synchronized代码块或者synchronized方法,在此等待获取锁的过程线程都处于阻塞状态。

reenter a synchronized block/method after calling Object#wait()

  1. 线程X步入synchronized代码块或者synchronized方法后(此时已经释放监视器锁)调用Object#wait()方法以后进行阻塞,当接收其余线程T调用该锁对象Object#notify()/notifyAll(),可是线程T还没有退出它所在的synchronized代码块或者synchronized方法,那么线程X依然处于阻塞状态(注意API注释中的reenter,理解它场景2就豁然开朗)。

更加详细的描述能够参考笔者以前写过的一篇文章:深刻理解Object提供的阻塞和唤醒API

针对上面的场景1举个简单的例子:

public class ThreadState6 {

    private static final Object MONITOR = new Object();

    public static void main(String[] args) throws Exception {
        Thread thread1 = new Thread(()-> {
            synchronized (MONITOR){
                try {
                    Thread.sleep(Integer.MAX_VALUE);
                } catch (InterruptedException e) {
                    //ignore
                }
            }
        });
        Thread thread2 = new Thread(()-> {
            synchronized (MONITOR){
                System.out.println("thread2 got monitor lock...");
            }
        });
        thread1.start();
        Thread.sleep(50);
        thread2.start();
        Thread.sleep(50);
        System.out.println(thread2.getState());
    }
}
// 输出结果
BLOCKED
复制代码

针对上面的场景2举个简单的例子:

public class ThreadState7 {

    private static final Object MONITOR = new Object();
    private static final DateTimeFormatter F = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");

    public static void main(String[] args) throws Exception {
        System.out.println(String.format("[%s]-begin...", F.format(LocalDateTime.now())));
        Thread thread1 = new Thread(() -> {
            synchronized (MONITOR) {
                System.out.println(String.format("[%s]-thread1 got monitor lock...", F.format(LocalDateTime.now())));
                try {
                    Thread.sleep(1000);
                    MONITOR.wait();
                } catch (InterruptedException e) {
                    //ignore
                }
                System.out.println(String.format("[%s]-thread1 exit waiting...", F.format(LocalDateTime.now())));
            }
        });
        Thread thread2 = new Thread(() -> {
            synchronized (MONITOR) {
                System.out.println(String.format("[%s]-thread2 got monitor lock...", F.format(LocalDateTime.now())));
                try {
                    MONITOR.notify();
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    //ignore
                }
                System.out.println(String.format("[%s]-thread2 releases monitor lock...", F.format(LocalDateTime.now())));
            }
        });
        thread1.start();
        thread2.start();
        // 这里故意让主线程sleep 1500毫秒从而让thread2调用了Object#notify()而且还没有退出同步代码块,确保thread1调用了Object#wait()
        Thread.sleep(1500);  
        System.out.println(thread1.getState());
        System.out.println(String.format("[%s]-end...", F.format(LocalDateTime.now())));
    }
}
// 某个时刻的输出以下:
[2019-06-20 00:30:22]-begin...
[2019-06-20 00:30:22]-thread1 got monitor lock...
[2019-06-20 00:30:23]-thread2 got monitor lock...
BLOCKED
[2019-06-20 00:30:23]-end...
[2019-06-20 00:30:25]-thread2 releases monitor lock...
[2019-06-20 00:30:25]-thread1 exit waiting...
复制代码

场景2中:

  • 线程2调用Object#notify()后睡眠2000毫秒再退出同步代码块,释放监视器锁。
  • 线程1只睡眠了1000毫秒就调用了Object#wait(),此时它已经释放了监视器锁,因此线程2成功进入同步块,线程1处于API注释中所述的reenter a synchronized block/method的状态。
  • 主线程睡眠1500毫秒恰好能够命中线程1处于reenter状态而且打印其线程状态,恰好就是BLOCKED状态。

这三点看起来有点绕,多看几回多思考一下应该就能理解。

TERMINATED状态

API注释

/** * Thread state for a terminated thread. * The thread has completed execution. */ 
TERMINATED;
复制代码

终结的线程对应的线程状态,此时线程已经执行完毕。

TERMINATED状态表示线程已经终结。一个线程实例只能被启动一次,准确来讲,只会调用一次Thread#run()方法,Thread#run()方法执行结束以后,线程状态就会更变为TERMINATED,意味着线程的生命周期已经结束。

举个简单的例子:

public class ThreadState8 {

    public static void main(String[] args) throws Exception {
        Thread thread = new Thread(() -> {

        });
        thread.start();
        Thread.sleep(50);
        System.out.println(thread.getState());
    }
}
// 输出结果
TERMINATED
复制代码

上下文切换

多线程环境中,当一个线程的状态由RUNNABLE转换为非RUNNABLEBLOCKEDWAITING或者TIMED_WAITING)时,相应线程的上下文信息(也就是常说的Context,包括CPU的寄存器和程序计数器在某一时间点的内容等等)须要被保存,以便线程稍后恢复为RUNNABLE状态时可以在以前的执行进度的基础上继续执行。而一个线程的状态由非RUNNABLE状态进入RUNNABLE状态时可能涉及恢复以前保存的线程上下文信息而且在此基础上继续执行。这里的对线程的上下文信息进行保存和恢复的过程就称为上下文切换(Context Switch)。

线程的上下文切换会带来额外的性能开销,这包括保存和恢复线程上下文信息的开销、对线程进行调度的CPU时间开销以及CPU缓存内容失效的开销(线程所执行的代码从CPU缓存中访问其所须要的变量值要比从主内存(RAM)中访问响应的变量值要快得多,可是线程上下文切换会致使相关线程所访问的CPU缓存内容失效,通常是CPU的L1 CacheL2 Cache,使得相关线程稍后被从新调度到运行时其不得再也不次访问主内存中的变量以从新建立CPU缓存内容)。

在Linux系统中,能够经过vmstat命令来查看全局的上下文切换的次数,例如:

$ vmstat 1
复制代码

对于Java程序的运行,在Linux系统中也能够经过perf命令进行监视,例如:

$ perf stat -e cpu-clock,task-clock,cs,cache-reference,cache-misses java YourJavaClass
复制代码

参考资料中提到Windows系统下能够经过自带的工具perfmon(其实也就是任务管理器)来监视线程的上下文切换,实际上笔者并无从任务管理器发现有任何办法查看上下文切换,经过搜索以后发现了一个工具:Process Explorer。运行Process Explorer同时运行一个Java程序而且查看其状态:

由于打了断点,能够看到运行中的程序的上下文切换一共7000屡次,当前一秒的上下文切换增量为26(由于笔者设置了Process Explorer每秒刷新一次数据)。

监控线程状态

若是项目在生产环境中运行,不可能频繁调用Thread#getState()方法去监测线程的状态变化。JDK自己提供了一些监控线程状态的工具,还有一些开源的轻量级工具如阿里的Arthas,这里简单介绍一下JDK自带的一些工具。

使用jvisualvm

jvisualvm是JDK自带的堆、线程等待JVM指标监控工具,适合使用于开发和测试环境。它位于JAVA_HOME/bin目录之下。

其中线程Dump的按钮相似于下面要提到的jstack命令,用于导出全部线程的栈信息。

使用jstack

jstack是JDK自带的命令行工具,功能是用于获取指定PID的Java进程的线程栈信息。例如本地运行的一个IDEA实例的PID是11376,那么只须要输入:

jstack 11376
复制代码

另外,若是想要定位具体Java进程的PID,可使用jps命令。

使用JMC

JMC也就是Java Mission Control,它也是JDK自带的任务监控工具,提供的功能要比jvisualvm强大,包括MBean的处理、线程栈已经状态查看、飞行记录器等等。

小结

理解Java线程状态的切换和一些监控手段,更有利于平常开发多线程程序,对于生产环境出现问题,经过监控线程的栈信息可以快速定位到问题的根本缘由(一般来讲,目前比较主流的MVC应用都是经过一个线程处理一个单独的请求,当请求出现阻塞的时候,导出对应处理请求的线程基本能够定位到阻塞的精准位置,若是使用消息队列例如RabbitMQ,消费者线程出现阻塞也能够利用类似的思路解决)。

参考资料:

  • Jdk11相关源码
  • 《Java多线程编程实战指南》
  • 《深刻理解Java虚拟机-2nd》

连接

(本文完 c-7-d e-a-20190623 最近业务迭代有点忙)

相关文章
相关标签/搜索