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

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

操作系统中的线程状态

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

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

  • 新建(NEW):线程已创建,但尚未开始执行。

  • 就绪(READY):线程等待使用 CPU,在被调度程序调用后可进入运行状态。

  • 运行(RUNNING):线程正在使用 CPU。

  • 等待(WAITING):线程因等待事件或其他资源(如 I/O)而被阻塞。

  • 终止(TERMINATED):线程已完成执行。

Java 线程的 6 种状态

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

java 复制代码
public enum State {
    NEW,
    RUNNABLE,
    BLOCKED,
    WAITING,
    TIMED_WAITING,
    TERMINATED;
}

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

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

NEW(新建)

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

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

输出:

sql 复制代码
NEW

RUNNABLE(可运行)

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

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

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

  • 能否对同一个线程重复调用start()方法?

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

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

java 复制代码
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()方法:

java 复制代码
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(); // 第二次调用
    }
}

输出:

java 复制代码
NEW
RUNNABLE
Exception 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)指的是状态检查失败:

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

java 复制代码
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 状态。

代码示例如下:

java 复制代码
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()来模拟业务处理所需的时间。

输出:

ini 复制代码
Thread[A] performs business processing
Thread[A] state:RUNNABLE
Thread[B] state:BLOCKED
Thread[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()实现,如下所示:

java 复制代码
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());
    }
}

输出:

ini 复制代码
Thread[A] 处理业务,但电脑坏了。
Thread[B] 维修电脑。
Thread[A] state:WAITING
Thread[B] state:TIMED_WAITING
Thread[B] 电脑维修好了。
Thread[A] 继续处理业务。

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

  • 线程在调用wait()方法之前必须持有对象的锁。

  • 当线程调用wait()方法时,它会释放当前的锁,直到另一个线程调用notify()notifyAll()方法唤醒等待锁的线程。

  • 调用notify()方法只会唤醒一个等待锁的线程。如果有多个线程在等待锁,之前调用wait()方法的线程可能不会被唤醒。

  • 调用notifyAll()方法后,所有等待锁的线程都会被唤醒,但时间片可能不会立即分配给刚刚放弃锁的线程,这取决于系统的调度。

Thread.join()

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

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

现在来银行办理业务的人越来越多了,如果每次窗口空闲出来后所有人都会争抢窗口的话,会造成资源的浪费。

银行想到了一个办法。每个来办理业务的客户都会得到一个序列号,窗口会依次叫号。只有被叫到的客户才需要去窗口,否则他们可以留在休息区。

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

java 复制代码
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();
    }
}

输出:

css 复制代码
请让线程 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()方法唤醒。

java 复制代码
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 对其进行了更细粒度的划分,以适应复杂的并发场景。掌握这些状态及其转换,是编写高效、稳定多线程程序的关键。

相关推荐
magic3341656313 分钟前
Springboot整合MinIO文件服务(windows版本)
windows·spring boot·后端·minio·文件对象存储
开心-开心急了23 分钟前
Flask入门教程——李辉 第一、二章关键知识梳理(更新一次)
后端·python·flask
掘金码甲哥34 分钟前
调试grpc的哼哈二将,你值得拥有
后端
陈小桔1 小时前
idea中重新加载所有maven项目失败,但maven compile成功
java·maven
小学鸡!1 小时前
Spring Boot实现日志链路追踪
java·spring boot·后端
xiaogg36781 小时前
阿里云k8s1.33部署yaml和dockerfile配置文件
java·linux·kubernetes
逆光的July1 小时前
Hikari连接池
java
微风粼粼1 小时前
eclipse 导入javaweb项目,以及配置教程(傻瓜式教学)
java·ide·eclipse
番茄Salad1 小时前
Spring Boot临时解决循环依赖注入问题
java·spring boot·spring cloud
天若有情6732 小时前
Spring MVC文件上传与下载全面详解:从原理到实战
java·spring·mvc·springmvc·javaee·multipart