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

开发 前端
Java 线程的六种状态(NEW、RUNNABLE、BLOCKED、WAITING、TIMED_WAITING、TERMINATED)描述了线程从创建到终止的完整生命周期。理解这些状态及其转换机制,有助于更好地掌握多线程编程,避免常见的并发问题。

在本文中,我们将深入探讨 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(); // 第二次调用
    }
}

输出:

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)指的是状态检查失败:

图片

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

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 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()实现,如下所示:

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

责任编辑:武晓燕 来源: 程序猿技术充电站
相关推荐

2020-11-13 08:42:24

Synchronize

2020-12-11 07:32:45

编程ThreadLocalJava

2021-09-18 06:56:01

JavaCAS机制

2022-10-12 07:53:46

并发编程同步工具

2019-07-24 16:04:47

Java虚拟机并发

2019-06-25 10:32:19

UDP编程通信

2023-10-27 07:47:58

Java语言顺序性

2021-07-26 07:47:37

无锁编程CPU

2024-05-17 12:56:09

C#编程线程

2024-03-19 14:14:27

线程开发

2018-03-14 15:20:05

Java多线程勘误

2017-12-18 16:33:55

多线程对象模型

2024-01-29 15:54:41

Java线程池公平锁

2023-10-08 09:34:11

Java编程

2023-09-19 22:47:39

Java内存

2009-06-19 14:10:42

Java多态性

2023-10-27 07:47:37

计算机内存模型

2018-03-22 18:30:22

数据库MySQL并发控制

2024-01-09 08:28:44

应用多线程技术

2024-06-06 09:58:13

点赞
收藏

51CTO技术栈公众号