1. 概述

在本文中,我们将详细讨论 Java 中的一个核心概念——线程的生命周期。

我们将使用一个简洁的图示和实际的代码片段来更好地理解线程执行过程中的这些状态。

要开始理解 Java 中的线程,这篇关于创建线程的文章是一个很好的起点。

2. Java 中的多线程

在 Java 语言中,多线程是由线程(Thread)的核心概念驱动的。 在它们的生命周期中,线程会经历各种状态:

线程生命周期

3. Java 中线程的生命周期

java.lang.Thread 类包含一个 静态 State 枚举——它定义了线程的潜在状态。在任何给定的时间点,线程只能处于以下状态之一:

  1. NEW——新创建的线程,尚未开始执行
  2. RUNNABLE——正在运行或准备执行,但正在等待资源分配
  3. BLOCKED——等待获取监视器锁以进入或重新进入同步块/方法
  4. WAITING——等待其他线程执行特定操作,没有时间限制
  5. TIMED_WAITING——等待其他线程在指定时间内执行特定操作
  6. TERMINATED——已完成执行

所有这些状态都在上图中涵盖;现在让我们详细讨论每一个状态。

3.1. 新建状态(New)

*一个 NEW Thread(或称为新生的 Thread)是已创建但尚未启动的线程。* 它保持此状态,直到我们使用 start() 方法启动它。

以下代码片段显示了一个处于 NEW 状态的新创建线程:

Runnable runnable = new NewState();
Thread t = new Thread(runnable);
System.out.println(t.getState());

由于我们尚未启动提到的线程,方法 t.getState() 会输出:

NEW

3.2. 可运行状态(Runnable)

当我们创建一个新线程并对其调用 start() 方法时,它会从 NEW 状态转换到 RUNNABLE 状态。此状态的线程要么正在运行,要么准备运行,但它们正在等待系统的资源分配。

在多线程环境中,线程调度器(Thread-Scheduler,是 JVM 的一部分)为每个线程分配固定的时间量。因此它会运行特定的时间量,然后将控制权让给其他 RUNNABLE 线程。

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

Runnable runnable = new NewState();
Thread t = new Thread(runnable);
t.start();
System.out.println(t.getState());

这段代码最有可能返回输出为:

RUNNABLE

注意,在这个例子中,并不保证当我们的控制到达 t.getState() 时,它仍然处于 RUNNABLE 状态。

它可能立即被 Thread-Scheduler 调度并完成执行。在这种情况下,我们可能会得到不同的输出。

3.3. 阻塞状态(Blocked)

当线程当前不符合运行条件时,它处于 BLOCKED 状态。当它等待监视器锁并尝试访问被其他线程锁定的代码段时,它会进入此状态。

让我们尝试重现这个状态:

public class BlockedState {
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(new DemoBlockedRunnable());
        Thread t2 = new Thread(new DemoBlockedRunnable());
        
        t1.start();
        t2.start();
        
        Thread.sleep(1000);
        
        System.out.println(t2.getState());
        System.exit(0);
    }
}

class DemoBlockedRunnable implements Runnable {
    @Override
    public void run() {
        commonResource();
    }
    
    public static synchronized void commonResource() {
        while(true) {
            // 无限循环以模拟繁重处理
            // 't1' 不会离开此方法
            // 当 't2' 尝试进入时
        }
    }
}

在这段代码中:

  1. 我们创建了两个不同的线程——t1t2
  2. t1 启动并进入同步的 commonResource() 方法;这意味着只有一个线程可以访问它;所有其他尝试访问此方法的后续线程将被阻止进一步执行,直到当前线程完成处理
  3. t1 进入此方法时,它被保持在一个无限的 while 循环中;这只是为了模拟繁重处理,以便所有其他线程无法进入此方法
  4. 现在当我们启动 t2 时,它尝试进入 commonResource() 方法,该方法已被 t1 访问,因此 t2 将被保持在 BLOCKED 状态

处于此状态时,我们调用 t2.getState() 并得到输出:

BLOCKED

3.4. 等待状态(Waiting)

当线程等待其他线程执行特定操作时,它处于 WAITING 状态。 根据 JavaDocs,任何线程都可以通过调用以下三种方法之一进入此状态:

  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 DemoWaitingStateRunnable());
        t2.start();

        try {
            t2.join();
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            e.printStackTrace();
        }
    }
}

class DemoWaitingStateRunnable implements Runnable {
    public void run() {
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            e.printStackTrace();
        }
        
        System.out.println(WaitingState.t1.getState());
    }
}

让我们讨论一下我们在这里做什么:

  1. 我们创建并启动了 t1
  2. t1 创建了一个 t2 并启动它
  3. t2 的处理继续进行时,我们调用 *t2.join()*,这将使 t1 处于 WAITING 状态,直到 t2 完成执行
  4. 由于 t1 正在等待 t2 完成,我们从 t2 调用 t1.getState()

这里的输出,正如你所期望的:

WAITING

3.5. 计时等待状态(Timed Waiting)

当线程等待其他线程在规定时间内执行特定操作时,它处于 TIMED_WAITING 状态。

根据 JavaDocs,有五种方法可以将线程置于 TIMED_WAITING 状态:

  1. thread.sleep(long millis)
  2. wait(int timeout)wait(int timeout, int nanos)
  3. thread.join(long millis)
  4. LockSupport.parkNanos
  5. LockSupport.parkUntil

要阅读更多关于 Java 中 wait()sleep() 之间差异的内容,请查看这里的专门文章

现在,让我们尝试快速重现这个状态:

public class TimedWaitingState {
    public static void main(String[] args) throws InterruptedException {
        DemoTimeWaitingRunnable runnable= new DemoTimeWaitingRunnable();
        Thread t1 = new Thread(runnable);
        t1.start();
        
        // 以下 sleep 将给 ThreadScheduler 足够的时间
        // 来开始处理线程 t1
        Thread.sleep(1000);
        System.out.println(t1.getState());
    }
}

class DemoTimeWaitingRunnable implements Runnable {
    @Override
    public void run() {
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            e.printStackTrace();
        }
    }
}

在这里,我们创建并启动了一个线程 t1,它进入睡眠状态,超时时间为 5 秒;输出将是:

TIMED_WAITING

3.6. 终止状态(Terminated)

这是死线程的状态。当线程已完成执行或被异常终止时,它处于 TERMINATED 状态。

我们有专门的文章讨论了停止线程的不同方法。

让我们尝试在以下示例中实现这个状态:

public class TerminatedState implements Runnable {
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(new TerminatedState());
        t1.start();
        // 以下 sleep 方法将给线程 t1 足够的时间完成
        Thread.sleep(1000);
        System.out.println(t1.getState());
    }
    
    @Override
    public void run() {
        // 此块中无处理
    }
}

在这里,虽然我们启动了线程 t1,但紧接着的语句 Thread.sleep(1000) 给了 t1 足够的时间来完成,所以这个程序给我们输出:

TERMINATED

除了线程状态外,我们还可以检查 isAlive() 方法来确定线程是否存活。例如,如果我们在这个线程上调用 isAlive() 方法:

Assert.assertFalse(t1.isAlive());

它返回 false。简单地说,线程当且仅当它已被启动且尚未死亡时才存活。

5. 总结

在本教程中,我们学习了 Java 中线程的生命周期。我们查看了 Thread.State 枚举定义的所有六个状态,并通过快速示例重现了它们。

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

并且,一如既往,这里使用的代码片段可在 GitHub 上获得。


« 上一篇: Java Weekly, 第216期
» 下一篇: Java实现数独求解器