跳到主要内容

17、秒懂 Java Thread 生命周期

本文中,我想详细的讨论下 Java 中的核心概念 - 线程的生命周期。我会使用一张我自制的图片加上实用的代码片段,一步一步的详细剖析线程的各个状态和各个状态之间如何转换。

Java 中的多线程

Java 语言中, 多线程是由 Thread 的核心概念驱动的。因为多线程中的每一个线程都相互独立,有着自己的生命周期和状态转换。

我们先来看一张草图,这图描述了 Java 线程的各种状态和转换过程。

 

是不是很杂乱无章? 看不懂没关系,我们接下来会详细介绍各个状态。

Java 线程中的生命周期

Java 中,每一个线程都是 java.lang.Thread 类的实例。而且,Java 个线程生命周期中的各个状态都定义在 Thread 类的一个静态的 State 枚举中。

State 枚举定义了线程的所有潜在状态。总共有 6 个,分别对应者上图中的 6 个绿色背景的矩形和椭圆型。

  • NEW : 新创建的,且未调用 start() 方法开始执行的线程。
  • RUNNABLE : 已经在运行中的线程或正在等待资源分配的准备运行的线程。
  • BLOCKED : 等待获取进入或重新进入同步块或方法的监视器锁的线程。
  • WAITING : 等待其他一些线程执行特定操作,没有任何时间限制。
  • TIMED_WAITING: 等待某个其他线程在指定时间段内执行特定操作
  • TERMINATED : 线程完成了它的任务。

需要注意的是: 在任何给定的时间点,线程只能处于这些状态之一

  • NEW 状态,应该很好理解,比如,车,厂家生产出来,只要还没被卖出过,那么它就是新的 ( NEW )
  • RUNNABLE 只要线程不出于其它状态,它就是 RUNNABLE 状态。怎么理解呢? 车买来了,只要它没坏没出什么毛病没借给别人,那么它就出于可开状态,不管是呆在家里吃灰还是已经在上路运行。
  • WAITING : 无时间显示的等待其它线程完成任务时就处于这个状态,怎么理解呢?比如长假告诉公路大堵车,要等待别人前进了几个蜗牛步我们才能往前几个蜗牛步,有时候一等就是昏天暗地,可能长达几天,也可能,一辈子吧。
  • TIMED_WAITING : 一直处于 WAITING 总不是办法,所以可能会设置一个超时时间,如果过了时间,就不等待了。同样的,如果可以后退,那么我们在堵车的时候可能会等待那么十几分钟,发现确实走不了,就等了呗。
  • TERMINATED : 当一个线程结束了它的任务(可能完成了,也可能没完成)就会处于这个状态。如果拿车做比喻,那么当车彻底报废,已经再也不能上路了,就处于这个状态。

其实拿车作比喻感觉有点怪,我觉得拿追女朋友来做比喻比较恰当些。

NEW 状态

NEW状态的线程(或已经创建的新线程)是已创建但尚未启动的线程。线程会一直保持这个 NEW 状态,直到在该线程上调用了 start() 方法启动它。

下面的代码,我们创建了一个 NEW 状态的线程

Runnable runnable = new NewState();
Thread t = new Thread(runnable);
Log.info(t.getState());

由于我们没有启动线程,因此 t.getState() 方法将打印输出

NEW

RUNNABLE 状态

当在一个 NEW 状态的线程上调用 start() 方法时,该线程的状态会从 NEW 转换为 RUNNABLE。处于该状态的线程要么是已经在运行中,那么是在处于正在等待系统的资源分配(准备运行)。

在多线程环境中,线程调度器 ( Thread-Scheduler,它是 JVM 的一部分)会为每个线程分配固定的时间。线程并不是一直都在执行的,调度器会把暂时空闲的线程的 CPU ( 还是在 RUNNABLE 状态 )让出来,让其它需要的线程去运行。因此它会运行一段特定的时间,然后将控制权放弃给其他 RUNNABLE 线程。

注意: 这里的等待资源,不是等待其它线程,而是等待 CPU 排队。打个比方,新车上路。要等待的是有没有路,如果没有路,就开不了,这是本质的问题。

例如,让我们将 t.start() 方法添加到我们之前的代码中并尝试访问其当前状态

Runnable runnable = new NewState();
Thread t = new Thread(runnable);
t.start();
Log.info(t.getState());

此代码最有可能返回输出

RUNNABLE

为什么说是最有可能呢?如果是一个空转线程,除了 CPU 不需要其它资源,那么很大概率就是 RUNNABLE ,但如果需要其它资源,可能会因为竞争资源而处于其它状态。还有一种情况,可能还没运行到 t.getState() ,线程任务就执行完毕了,那么也不会是 RUNNABLE 状态。

BLOCKED 状态

当一个线程当前没有资格运行时,它处于 BLOCKED 状态。如果线程在尝试访问由某个其他线程锁定的代码段时,那它会因为需要等待获取监视器锁进入此状态。

我们使用一小段代码来重现下这个状态

public class BlockedState {
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(new DemoThreadB());
        Thread t2 = new Thread(new DemoThreadB());

        t1.start();
        t2.start();

        Thread.sleep(1000);

        Log.info(t2.getState());
        System.exit(0);
    }
}

class DemoThreadB implements Runnable {
    @Override
    public void run() {
        commonResource();
    }

    public static synchronized void commonResource() {
        while(true) {
            // Infinite loop to mimic heavy processing
            // 't1' won't leave this method
            // when 't2' try to enters this
        }
    }
}

在上面这段代码中

  • 我们创建了两个不同的线程-- t1 和 t2 。
  • t1 启动后就进入了同步的 commonResource()方法,同步方法意味着一次只能有一个线程可以访问它。尝试访问此方法的所有其他后续线程将被阻止进一步执行,直到当前线程完成处理。
  • 当 t1 进入这个方法时,它保持了无限循环,这只是为了模仿繁重的处理,以便所有其他线程都无法进入此方法。
  • 接着我们开启 t2 ,它尝试输入已经被 t1 访问的 commonResource() 方法,这时,因为 commonResource() 被 t1 锁定,所以 t2 将保持在 BLOCKED 状态

在这个状态上,当我们使用 t.getState() 时将输出

BLOCKED

WAITTING 状态

线程在等待某个其他线程执行特定操作时处于 WAITING 状态。根据 Oracle 官方文档,任何线程都可以通过调用以下三种方法中的任何一种来进入此状态:

1、 object.wait();
2、 thread.join();
3、 LockSupport.park();

请注意,我们没有为 wait() 和 join() 定义任何超时时间,因为下一节将介绍该方案。

我们以后会写一个单独的教程,详细讨论了 wait()、notify() 和 notifyAll() 的使用。

下面,我们写一段代码尝试重现这种状态

public class WaitingState implements Runnable {
    public static Thread t1;

    public static void main(String[] args) {
        t1 = new Thread(new WaitingState());
        t1.start();
    }

    public void run() {
        Thread t2 = new Thread(new DemoThreadWS());
        t2.start();

        try {
            t2.join();
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            Log.error("Thread interrupted", e);
        }
    }
}

class DemoThreadWS implements Runnable {
    public void run() {
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            Log.error("Thread interrupted", e);
        }

        Log.info(WaitingState.t1.getState());
    }
}

我们来讨论一下上面的代码做的事情

1、 首先,我们创建并启动了t1;
2、 其次,t1创建了t2并启动它;
3、 当t2的处理继续时,我们调用t2.join(),这使t1处于WAITING状态,直到t2完成执行;
4、 由于t1正在等待t2完成,我们从t2调用t1.getState();

输出结果一般为

WAITING

请留意在哪里调用 t1.getState() 。

所以,WAITING 和 BLOCKED 两个状态的区别是什么?

  • BLOCKED 是因为线程竞争不到资源而处于 BLOCKED 状态。这个是被动的。因为别无选择。
  • WAITING 是因为线程主动等待别人完成而处于 WAITING 状态。这个是主动的。因为它可以不调用那三个方法,不用等待其它人完成。它可以选择挥一挥衣袖,不不带走一片云彩

TIMED_WAITING 状态

线程在等待另一个线程在规定的时间内执行特定操作时处于 TIMED_WAITING 状态。根据 Java Docs 文档,有五种方法可以将线程置于TIMED_WAITING 状态:

1、 thread.sleep(longmillis);
2、 wait(inttimeout)orwait(inttimeout,intnanos);
3、 thread.join(longmillis);
4、 LockSupport.parkNanos;
5、 LockSupport.parkUntil;

下面,我们写一段代码尝试重现这种状态

public class TimedWaitingState {
    public static void main(String[] args) throws InterruptedException {
        DemoThread obj1 = new DemoThread();
        Thread t1 = new Thread(obj1);
        t1.start();

        // The following sleep will give enough time for ThreadScheduler
        // to start processing of thread t1
        Thread.sleep(1000);
        Log.info(t1.getState());
    }
}

class DemoThread implements Runnable {
    @Override
    public void run() {
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            Log.error("Thread interrupted", e);
        }
    }
}

整体代码和 WAITING 状态的差不多,我们创建并启动了一个线程 t1,并它进入睡眠状态,超时时间为 5 秒。

输出结果为

TIMED_WAITING

TERMINATED 状态

这是一个 「 已死 」 线程的状态。当一个线程已经完成执行或异常终止时,它处于 TERMINATED 状态。我们在 怎么关闭一个 Java 线程 ( Thread ) ? 一文中讨论过杀死线程的不同方法。

这个状态没什么好讨论的,我们写一段代码尝试重现这种状态

public class TerminatedState implements Runnable {
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(new TerminatedState());
        t1.start();
        // The following sleep method will give enough time for 
        // thread t1 to complete
        Thread.sleep(1000);
        Log.info(t1.getState());
    }

    @Override
    public void run() {
        // No processing in this block
    }
}

上面这段代码中,我们启动线程 t1 时,下一个语句 Thread.sleep(1000) 为 t1 提供了足够的时间来完成。

因此,上面这个示例输出结果为

TERMINATED

后记

在本教程中,我们了解了 Java 中线程的生命周期。我们详细介绍了 Thread.State 枚举定义的所有七个状态,并使用一些示例来演示他们。

虽然代码片段几乎可以在每台机器上提供相同的输出,但在某些特殊情况下,我们可能会得到一些不同的输出,因为线程调度程序的确切行为无法确定。

所以,有任何问题,欢迎回帖咨询