四时宝库

程序员的知识宝库

Java并发编程:深入理解Java线程状态

在本文中,我们将深入探讨 Java 线程的六种状态以及它们之间如何相互转换。线程状态的转换就如同生物从出生、成长到最终死亡的过程,也有一个完整的生命周期。

操作系统中的线程状态

首先,让我们看看操作系统中线程的生命周期是如何流转的。

在操作系统中,线程共有 5 种状态:

  • 新建(NEW):线程已创建,但尚未开始执行。
  • 就绪(READY):线程等待使用 CPU,在被调度程序调用后可进入运行状态。
  • 运行(RUNNING):线程正在使用 CPU。
  • 等待(WAITING):线程因等待事件或其他资源(如 I/O)而被阻塞。
  • 终止(TERMINATED):线程已完成执行。

Java 线程的 6 种状态

Java 中线程状态的定义与操作系统中的并不完全相同,查看 JDK 中的java.lang.Thread.State可以找到 Java 线程状态的定义:

public enum State {    NEW,    RUNNABLE,    BLOCKED,    WAITING,    TIMED_WAITING,    TERMINATED;}

它们之间的流程关系如下图所示:

接下来,我们将对 Java 线程的六种状态进行深入分析。

NEW(新建)

处于NEW状态的线程实际上还没有启动。也就是说,Thread 实例的start()方法还没有被调用。可流转状态:RUNNABLE

public class ThreadStateDemo {    public static void main(String[] args) {        Thread thread = new Thread(() -> {});        System.out.println(thread.getState());    }}

输出:

NEW

RUNNABLE(可运行)

Java 中的Runable状态对应操作系统线程状态中的两种状态,分别是RunningReady,也就是说,Java 中处于Runnable状态的线程有可能正在执行,也有可能没有正在执行比如正在等待被分配 CPU 资源。

所以,如果一个正在运行的线程是Runnable状态,当它运行到任务的一半时,执行该线程的 CPU 被调度去做其他事情,导致该线程暂时不运行,它的状态依然不变,还是Runnable,因为它有可能随时被调度回来继续执行任务。可流转状态:BLOCKEDWAITINGTIMED_WAITINGTERMINATED在 Java 中,线程通过调用Thread实例的start()方法进入RUNNABLE状态。

关于start()方法,有两个问题需要思考一下:

  • 能否对同一个线程重复调用start()方法?
  • 如果一个线程已经执行完毕并处于TERMINATED状态,是否可以再次调用该线程的start()方法?

为了分析这两个问题,我们先来看看start()方法的源码:

public synchronized void start() {    if (threadStatus!= 0)        thrownew IllegalThreadStateException();    group.add(this);    boolean started = false;    try {        start0();        started = true;    } finally {        try {            if (!started) {                group.threadStartFailed(this);            }        } catch (Throwable ignore) {        }    }}

我们可以看到,在start()方法内部,有一个threadStatus变量。如果它不等于 0,调用start()方法将直接抛出异常。

接下来,调用了一个start0()方法,但它是一个本地方法,无法知道方法内如何处理threadStatus。但没关系,我们可以在调用start()方法后输出当前状态,并尝试再次调用start()方法:

public class ThreadStateDemo {    public static void main(String[] args) {        Thread thread = new Thread(() -> {});        System.out.println(thread.getState());        thread.start(); // 第一次调用        System.out.println(thread.getState());        thread.start(); // 第二次调用    }}

输出:

NEWRUNNABLEException in thread "main" java.lang.IllegalThreadStateException    at java.lang.Thread.start(Thread.java:708)    at thread.basic.ThreadStateDemo.main(ThreadStateDemo.java:11)

可以看到,第一次调用start()方法是可以的,但第二次调用会报错,java.lang.Thread.start(Thread.java:708)指的是状态检查失败:

查看获取当前线程状态的源码:

public State getState() {    // 获取当前线程状态    return sun.misc.VM.toThreadState(threadStatus);}public static State toThreadState(int var0) {    if ((var0 & 4) != 0) {        return State.RUNNABLE;    } elseif ((var0 & 1024) != 0) {        return State.BLOCKED;    } elseif ((var0 & 16) != 0) {        return State.WAITING;    } elseif ((var0 & 32) != 0) {        return State.TIMED_WAITING;    } elseif ((var0 & 2) != 0) {        return State.TERMINATED;    } else {        return (var0 & 1) == 0 ? State.NEW : State.RUNNABLE;    }}

我们可以看到,只有State.NEW的状态值被计算为 0。

因此,结合上面的源码,我们可以得到两个问题的答案都是不可行的。start()方法只能在NEW状态下调用。

BLOCKED(阻塞)

处于BLOCKED状态的线程正在等待锁的释放。可流转状态:RUNNABLE我们用一个生活中的例子来说明BLOCKED状态:

假设你去银行办理业务。当你来到某个窗口时,发现前面已经有人了。这时,你必须等待前面的人离开窗口,才能办理业务。
假设你是线程 B,前面的人是线程 A。此时,A 占有了锁(银行办理业务的窗口),B 正在等待锁的释放,线程 B 此时就处于 BLOCKED 状态。

代码示例如下:

public class BlockCase {    private synchronized void businessProcessing() {        try {            System.out.println("Thread[" + Thread.currentThread().getName() + "] performs business processing");            Thread.sleep(2000L);        } catch (InterruptedException e) {            e.printStackTrace();        }    }    public static void main(String[] args) {        BlockCase blockCase = new BlockCase();        Thread A = new Thread(blockCase::businessProcessing, "A");        Thread B = new Thread(blockCase::businessProcessing, "B");        A.start();        B.start();        System.out.println("Thread[" + A.getName() + "] state:" + A.getState());        System.out.println("Thread[" + B.getName() + "] state:" + B.getState());    }}

这里使用Thread.sleep()来模拟业务处理所需的时间。

输出:

Thread[A] performs business processingThread[A] state:RUNNABLEThread[B] state:BLOCKEDThread[B] performs business processing
注意:如果多次执行输出结果可能不相同,这是因为两个线程谁先被调度是随机的

WAITING(等待)

等待状态。处于等待状态的线程需要其他线程唤醒才能转换为RUNNABLE状态。可流转状态:RUNNABLE调用以下三种方法会使线程进入等待状态:

  • Object.wait():使当前线程进入等待状态,直到另一个线程唤醒它;
  • Thread.join():等待指定的线程执行完毕。底层调用的是Object实例的wait方法;
  • LockSupport.park():在获得调用权限之前禁止当前线程进行线程调度。

我们主要解释Object.wait()Thread.join()的用法。

继续前面的例子来解释 WAITING 状态:

你在银行等了很久,终于轮到你来办理业务了。但不幸的是,你到达柜台后,柜台的电脑突然坏了。你必须等待维修人员修好电脑后才能继续办理业务。
此时,假设你是线程 A,维修人员是线程 B。虽然你已经拥有了锁(窗口),但你仍然需要释放锁。此时,线程 A 的状态是 WAITING,然后线程 B 获得锁并进入 RUNNABLE 状态。
如果线程 B 没有主动唤醒线程 A(通过notify()notifyAll()),线程 A 只能一直等待。

Object.wait()

对于这个例子,我们使用wait()notify()实现,如下所示:

public class WaitingCase {    private synchronized void businessProcessing() {        try {            System.out.println("Thread[" + Thread.currentThread().getName() + "] 处理业务,但电脑坏了。");            // 释放窗口资源(锁)            wait();            // 业务处理            System.out.println("Thread[" + Thread.currentThread().getName() + "] 继续处理业务。");            Thread.sleep(2000L);        } catch (InterruptedException e) {            e.printStackTrace();        }    }    private synchronized void repairComputer() {        System.out.println("Thread[" + Thread.currentThread().getName() + "] 维修电脑。");        try {            // 模拟维修            Thread.sleep(1000);            System.out.println("Thread[" + Thread.currentThread().getName() + "] 电脑维修好了。");            notify();        } catch (InterruptedException e) {            e.printStackTrace();        }    }    public static void main(String[] args) throws InterruptedException {        WaitingCase blockedCase = new WaitingCase();        Thread A = new Thread(blockedCase::businessProcessing, "A");        Thread B = new Thread(blockedCase::repairComputer, "B");        A.start();        Thread.sleep(500); // 用于确保线程 A 先抢到锁。睡眠时间应该小于维修时间        B.start();        System.out.println("Thread[" + A.getName() + "] state:" + A.getState());        System.out.println("Thread[" + B.getName() + "] state:" + B.getState());    }}

输出:

Thread[A] 处理业务,但电脑坏了。Thread[B] 维修电脑。Thread[A] state:WAITINGThread[B] state:TIMED_WAITINGThread[B] 电脑维修好了。Thread[A] 继续处理业务。

关于wait()方法,这里有一些需要注意的点:

  • 线程在调用wait()方法之前必须持有对象的锁。
  • 当线程调用wait()方法时,它会释放当前的锁,直到另一个线程调用notify()notifyAll()方法唤醒等待锁的线程。
  • 调用notify()方法只会唤醒一个等待锁的线程。如果有多个线程在等待锁,之前调用wait()方法的线程可能不会被唤醒。
  • 调用notifyAll()方法后,所有等待锁的线程都会被唤醒,但时间片可能不会立即分配给刚刚放弃锁的线程,这取决于系统的调度。

Thread.join()

join()方法暂停调用线程的执行,直到被调用的对象完成执行。此时,当前线程处于WAITING状态。

join()方法通常在主线程中使用,以等待其他线程完成后主线程再继续执行。

现在来银行办理业务的人越来越多了,如果每次窗口空闲出来后所有人都会争抢窗口的话,会造成资源的浪费。
银行想到了一个办法。每个来办理业务的客户都会得到一个序列号,窗口会依次叫号。只有被叫到的客户才需要去窗口,否则他们可以留在休息区。

让我们扩展前面BlockCase中的例子来简单实现这样的功能:

public class JoinCase {    private synchronized void businessProcessing() {        try {            System.out.println("Thread[" + Thread.currentThread().getName() + "] 办理业务。");            Thread.sleep(2000L);        } catch (InterruptedException e) {            e.printStackTrace();        }    }    public static void main(String[] args) throws InterruptedException {        JoinCase blockedCase = new JoinCase();        Thread A = new Thread(blockedCase::businessProcessing, "A");        Thread B = new Thread(blockedCase::businessProcessing, "B");        Thread C = new Thread(blockedCase::businessProcessing, "C");        System.out.println("请让线程 A 到窗口处理业务。");        A.start();        A.join();        System.out.println("请让线程 B 到窗口处理业务。");        B.start();        B.join();        System.out.println("请让线程 C 到窗口处理业务。");        C.start();    }}

输出:

请让线程 A 到窗口处理业务。Thread[A] 办理业务。请让线程 B 到窗口处理业务。Thread[B] 办理业务。请让线程 C 到窗口处理业务。Thread[C] 办理业务。

你可以多次尝试执行这个程序,每次都会得到相同的结果。

TIMED_WAITING(超时等待)

超时等待状态。线程等待特定的时间,时间到了会自动唤醒。可流转状态:RUNNABLE

调用以下方法会使线程进入超时等待状态:

  • Thread.sleep(long millis):使当前线程睡眠指定的时间,不释放锁;
  • Object.wait(long timeout):线程等待指定的时间。在等待期间,可以通过notify()/notifyAll()唤醒;
  • Thread.join(long millis):等待指定线程执行最多millis毫秒。如果millis为 0,则会继续执行;
  • LockSupport.parkNanos(long nanos):在获得调用权限之前,禁止当前线程进行线程调度指定的纳秒时间;
  • LockSupport.parkUntil(long deadline):与上述类似,也禁止线程调度指定的时间。

我们继续上面的例子来解释 TIMED_WAITING 状态:

当你轮到你办理业务员时,之前办理业务的客户说他忘记处理一个业务,现在需要处理,要求你给他 5 分钟时间。你同意了然后就去休息区休息,当 5 分钟过去后,你重新去办理业务。
此时,你仍然是线程 A,插队的朋友是线程 B。线程 B 让线程 A 等待指定的时间,在这段等待期间,A 处于 TIMED_WAITING 状态。
等待 5 分钟后,A 自动唤醒,获得了竞争锁(窗口)的资格。

可以使用Object.wait(long timeout)方法实现。Object.wait(long timeout)方法与无参数的wait()方法功能相同,都可以被其他线程调用notify()notifyAll()方法唤醒。

public class TimedWaitingCase {    privatestaticfinal Object lock = new Object();    public static void main(String[] args) {        // 线程 A:模拟等待超时        Thread threadA = new Thread(() -> {            synchronized (lock) {                try {                    System.out.println("线程 A 开始等待,最多等待 5 秒...");                    // 线程 A 进入 TIMED_WAITING 状态,等待 5 秒                    lock.wait(5000);                    System.out.println("线程 A 等待结束,继续执行。");                } catch (InterruptedException e) {                    e.printStackTrace();                }            }        });        // 线程 B:模拟在等待期间唤醒线程 A        Thread threadB = new Thread(() -> {            synchronized (lock) {                try {                    // 线程 B 先睡眠 2 秒,模拟一些处理时间                    Thread.sleep(2000);                    System.out.println("线程 B 尝试唤醒等待的线程 A...");                    // 唤醒等待的线程 A                    lock.notify();                } catch (InterruptedException e) {                    e.printStackTrace();                }            }        });        // 启动线程 A        threadA.start();        // 启动线程 B        threadB.start();    }}

不同之处在于,带参数的wait(long)方法即使没有其他线程唤醒它,也会在指定时间后自动唤醒,使其获得竞争锁的资格。

TERMINATED(终止)

再来看看最后一种状态,Terminated终止状态,要想进入这个状态有两种可能。

  • run()方法执行完毕,线程正常退出。
  • 出现一个没有捕获的异常,终止了run()方法,最终导致意外终止。

可流转状态:无

总结

Java 线程的六种状态(NEW、RUNNABLE、BLOCKED、WAITING、TIMED_WAITING、TERMINATED)描述了线程从创建到终止的完整生命周期。理解这些状态及其转换机制,有助于更好地掌握多线程编程,避免常见的并发问题。Java 线程状态与操作系统线程状态虽有相似之处,但 Java 对其进行了更细粒度的划分,以适应复杂的并发场景。掌握这些状态及其转换,是编写高效、稳定多线程程序的关键。

发表评论:

控制面板
您好,欢迎到访网站!
  查看权限
网站分类
最新留言
    友情链接