当前位置: 首页 > article >正文

Java并发编程实战 03 | Java线程状态

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

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

操作系统中的线程状态转换

线程在操作系统中通常有五种状态。

在现代操作系统中,线程被视为轻量级进程,因此操作系统中的线程状态实际上与进程状态类似。

从实际意义上讲,除了 new 和 terminated 状态外,线程主要有以下三种状态:

  • 就绪 (Ready) :线程已准备好执行,但可能由于调度策略或其他因素未能获得 CPU ,处于就绪状态的线程只要获得CPU,它就会进入运行状态。
  • 正在运行(RUNNING):线程当前正在占用CPU,执行其任务。
  • 等待(WAITING):线程正在等待某些事件的发生或资源的获取(例如I/O操作)。

new 和 terminated 状态在线程的实际运行过程中并不频繁涉及,因此讨论这两个状态在实际应用中并不具有太大意义。

Java 线程的 6 种状态

在 Java 中,线程状态的定义与操作系统中的状态并不完全相同。Java 的线程状态提供了更细粒度的管理。

Java 线程状态通过 java.lang.Thread.State 枚举进行定义:

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

这些状态之间的转换关系如下图所示:

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

1. NEW

线程对象被创建出来但是start() 方法还没有被调用,这个时候线程处于new状态。

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

//输出:
NEW

处于new状态的线程可以扭转到RUNNABLE状态。

2. RUNNABLE

处于new状态的线程,通过调用Thread实例的start()方法可以使线程进入RUNNABLE状态。

注意,Java线程中的RUNNABLE状态对应的是操作系统中线程定义的两种 状态:

  1. 就绪 (Ready)
  2. 正在运行(RUNNING)

也就是说在Java中,当一个线程正在运行时,如果CPU时间片用完了, CPU 被调度去执行其他任务,导致该线程暂时停止运行,它的状态仍保持为 RUNNABLE。因为该线程随时可能被重新调度回 CPU 上继续执行。

一个简单的线程示例:

public class ThreadExample {
    public static void main(String[] args) {
        // 创建一个线程对象,但尚未启动
        Thread myThread = new Thread(() -> {
            System.out.println("线程正在运行...");
        });

        // 打印线程状态,是 NEW
        System.out.println("线程状态: " + myThread.getState());

        // 启动线程
        myThread.start();

        // 打印线程状态,是 RUNNABLE(取决于线程调度)
        System.out.println("线程状态: " + myThread.getState());
    }
}

前面讲了处于NEW状态的线程,通过调用Thread实例的start()方法进入RUNNABLE状态。关于start()方法,其实有两个值得思考的问题:

  1. 是否可以在同一个线程对象上重复调用 start() 方法?
  2. 如果一个线程已经执行完毕,处于 TERMINATED 状态,是否可以再次调用 start() 方法?

要回答这两个问题,我们可以查看 start() 方法的源码。

public synchronized void start() {
    if (threadStatus != 0)
        throw new 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() 方法时,就会直接抛出 IllegalThreadStateException 异常。

接着,start() 方法调用了一个名为 start0() 的方法,该方法是一个本地方法(native method),由底层操作系统或虚拟机实现,因此我们无法从 Java 代码中看到它对 threadStatus 的具体处理方式。不过,这并不妨碍我们了解其行为。

我们可以通过在调用 start() 方法时打印出当前线程的状态,然后尝试多次调用 start() 方法,以观察并理解 IllegalThreadStateException 异常的触发条件和线程状态的变化。

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) 处,这个错误的原因是线程的状态检查失败。

这是因为当线程的 start() 方法第一次被调用时,线程被正确启动并进入 RUNNABLE 状态。第二次再调用start()时,由于线程的状态不再是初始NEW状态(0),直接抛出异常。

最后总结一下:

  1. 如果尝试在同一个线程上重复调用 start() 方法,会抛出 IllegalThreadStateException 异常,也就是说同一个线程对象只能启动一次。
  2. 如果线程已经完成(处于 TERMINATED 状态),再次调用 start() 方法也是不允许的,会同样抛出 IllegalThreadStateException 异常。线程一旦结束就不能再被重新启动。

处于RUNNABLE状态的线程根据不同的条件可以扭转到BLOCKED,WAITING,TIMED_WAITING,TERMINATED等状态。

3. BLOCKED

当线程处于 BLOCKED 状态时,表示它正在等待获取一个锁,以便进入同步区域。

我们可以用一个生活中的例子来说明 BLOCKED 状态:假设你去银行办理业务,当你走到某个窗口时,发现已经有一个人在你前面,此时你必须等到前面的人办完业务并离开窗口后,才能开始办理你的业务。

在这个例子中,你就是线程 B,前面的人是线程 A。当 A 正在占用窗口(即锁),而 B 需要等待 A 完成并释放窗口资源,这期间线程 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() 方法来模拟业务处理所需的时间。

你可能会觉得线程 A 会首先调用同步方法,而在同步方法内调用 Thread.sleep() 方法,使其进入 TIMED_WAITING 状态;与此同时,线程 B 则在等待线程 A 释放锁,因此它的状态会是 BLOCKED。然而,实际情况并不总是如此!这是因为:

  1. 除了线程 A 和线程 B,程序中还有一个主线程在运行。
  2. 当我们调用 start() 方法启动线程时,线程从调用 start() 到真正开始执行 run() 方法之间存在一定的时间差。在这个时间差内,CPU 的调度竞争结果会导致不同的输出。

下面是一种可能的输出:

//输出:
Thread[A] performs business processing
Thread[A] state:RUNNABLE
Thread[B] state:BLOCKED
Thread[B] performs business processing

这种场景下,线程 A 正在执行 businessProcessing 方法,线程 B 正在等待获取 businessProcessing 方法的锁,因此它处于 BLOCKED 状态。

如果你希望线程 A 打印出 TIMED_WAITING 状态,而线程 B 打印出 BLOCKED 状态,可以稍微修改主线程的逻辑。在调用 A.start() 后,让主线程“休息一会儿”,使用 Thread.sleep() 方法让线程 A 有时间去获取锁。

注意的是,主线程的休眠时间应该足够长,确保线程 A 正在执行并进入 TIMED_WAITING 状态,但又不应太长,以免线程 A 完成任务并释放锁。这样,在线程 A 执行期间,线程 B 仍然会尝试获取锁并进入 BLOCKED 状态。

这样一来,我们可以有效地控制两个线程的状态,使得线程 A 进入 TIMED_WAITING 状态,而线程 B 则处于 BLOCKED 状态。

public static void main(String[] args) throws InterruptedException {
        BlockCase blockCase = new BlockCase();
        Thread A = new Thread(blockCase::businessProcessing, "A");
        Thread B = new Thread(blockCase::businessProcessing, "B");

        // Fixed output TIMED_WAITING state and BLOCKED state

        A.start();
        Thread.sleep(1000); //Sleep time should be less than business processing time
        B.start();
        System.out.println("Thread[" + A.getName() + "] state:" + A.getState());
        Sy  stem.out.println("Thread[" + B.getName() + "] state:" + B.getState());

}

//输出:
Thread[A] performs business processing
Thread[A] state:TIMED_WAITING
Thread[B] state:BLOCKED
Thread[B] performs business processing 

在这个例子中,两个线程的状态会按照以下步骤转换:

线程 A 的状态转换过程:

  1. NEW: 线程 A 被创建,但还未启动。
  2. RUNNABLE: 调用 A.start() 后,线程 A 进入可运行状态,等待被 CPU 调度。
  3. TIMED_WAITING: 线程 A 获取到锁后,调用 Thread.sleep() 方法进入计时等待状态。
  4. RUNNABLE: 等待时间结束,线程 A 重新进入可运行状态。
  5. TERMINATED: 线程 A 执行完任务,进入终止状态。

线程 B 的状态转换过程:

  1. NEW: 线程 B 被创建,但还未启动。
  2. RUNNABLE: 调用 B.start() 后,线程 B 进入可运行状态,等待被 CPU 调度。
  3. BLOCKED: 线程 B 尝试获取锁失败,因为线程 A 已经持有锁,所以 B 进入阻塞状态。
  4. RUNNABLE: 线程 A 释放锁后,线程 B 获取到锁,进入可运行状态。
  5. TIMED_WAITING: 线程 B 进入临时等待状态(例如,通过 Thread.sleep())。
  6. RUNNABLE: 等待时间结束,线程 B 重新进入可运行状态。
  7. TERMINATED: 线程 B 执行完任务,进入终止状态。

处于BLOCKED状态的线程获取到锁后可以扭转到RUNNABLE状态。

4. WAITING

线程进入WAITING状态的方式有三种:

  1. Object.wait():将当前线程置于等待状态,直到另一个线程调用同一对象的 notify() 或 notifyAll() 方法来唤醒它。
  2. Thread.join():使当前线程等待指定的线程执行完毕后再继续运行。底层实现是调用 Object.wait() 方法。
  3. LockSupport.park():使当前线程进入等待状态,直到被显式地唤醒。它的控制权完全取决于是否获得了唤醒权限。

让我们继续用之前的银行办理业务的例子来解释 WAITING 状态。

假设你在银行办理业务时,终于轮到你到柜台办理了。但是,不幸的是,柜台的电脑突然坏了。为了完成业务,你必须等待维修人员修好电脑后才能继续办理。

在这个场景中,假设你是线程 A,维修人员是线程 B。尽管你已经在柜台前等待(即获得了锁),但是你还要释放锁,此时线程A的状态是WAITING,然后线程B获得锁,进入RUNNABLE状态。

如果线程 B 不主动唤醒线程 A(通过调用 notify() 或 notifyAll() 方法),线程 A 将会一直处于等待状态,无法继续执行。

以下是一个简单的代码示例,演示了如何使用 Object.wait() 和 notify() 方法来实现这种行为:

public class WaitingCase {

    private synchronized void businessProcessing() {
        try {
            System.out.println("Thread[" + Thread.currentThread().getName() + "] expects to process business, but the computer is broken");
            // Release the monitor(lock)
            wait();
            // business processing
            System.out.println("Thread[" + Thread.currentThread().getName() + "] continues to process business");
            Thread.sleep(2000L);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    private synchronized void repairComputer() {
        System.out.println("Thread[" + Thread.currentThread().getName() + "] comes to repair the computer");
        try {
            // Simulated Repair
            Thread.sleep(1000L);
            System.out.println("Thread[" + Thread.currentThread().getName() + "] has completed the repair.");
            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); //Used to ensure that thread A grabs the lock first. Sleep time should be less than repair time
        B.start();
        System.out.println("Thread[" + A.getName() + "] state:" + A.getState());
        System.out.println("Thread[" + B.getName() + "] state:" + B.getState());
    }
}

//输出:
Thread[A] expects to process business, but the computer is broken
Thread[B] comes to repair the computer
Thread[A] state:WAITING
Thread[B] state:TIMED_WAITING
Thread[B] has completed the repair.
Thread[A] continues to process business

关于 wait() 方法,有几个关键点需要特别强调:

  1. 持有锁:在调用 wait() 方法之前,线程必须先获得对象的监视器(锁)。换句话说,调用 wait() 的线程必须在同步代码块或同步方法中运行,即持有对象的锁。

  2. 释放锁:当线程调用 wait() 方法时,它会释放当前持有的锁,并进入等待状态。线程将保持在等待状态,直到其他线程调用 notify() 或 notifyAll() 方法来唤醒它。

  3. notify() 方法:调用 notify() 方法只能唤醒一个正在等待该锁的线程。如果有多个线程在等待同一个对象的锁,notify() 方法只会唤醒其中一个线程,这个线程并不是固定的,具体哪个线程被唤醒取决于线程调度的具体实现。

  4. notifyAll() 方法:调用 notifyAll() 方法会唤醒所有正在等待该锁的线程。这些被唤醒的线程会竞争重新获得锁,但并不保证它们会立即得到 CPU 时间片,具体的调度顺序取决于操作系统的线程调度策略。

我们再来看看Thread.join()方法。

join() 方法用于使调用线程暂停执行,直到被调用的线程执行完毕。调用 join() 的线程将进入 WAITING 状态,直到目标线程完成执行。这个方法常用于主线程中,确保在继续执行之前等待其他线程完成。

我们来回顾一下之前的 BlockCase 示例,其中 A.start() 和 B.start() 都是在主线程中直接调用的。这就像是让多个线程竞争窗口的使用权。如果参与竞争的线程越来越多,窗口就会变得非常拥挤。

为了改善这个问题,银行引入了一个新的办法:给每个办理业务的客户一个编号,按编号叫号。只有被叫到的客户才能到窗口办理业务,其余的客户则可以在休息区等待。

现在,我们可以在之前的 BlockCase 示例中扩展这个想法,假设我们有三个线程来模拟这个场景。每个线程代表一个客户,我们将使用 join() 方法来确保主线程等待所有客户(线程)完成业务后才继续执行。

public class JoinCase {

    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) 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("Please ask thread A to go to the window to handle the business.");
        A.start();
        A.join();
        System.out.println("Please ask thread B to go to the window to handle the business.");
        B.start();
        B.join();
        System.out.println("Please ask thread C to go to the window to handle the business.");
        C.start();
    }
}

//输出:
Please ask thread A to go to the window to handle the business.
Thread[A] performs business processing
Please ask thread B to go to the window to handle the business.
Thread[B] performs business processing
Please ask thread C to go to the window to handle the business.
Thread[C] performs business processing

您可以尝试多次执行该程序,并且总是会得到相同的结果。

处于WAITING状态的线程被其他线程唤醒可以扭转到RUNNABLE状态。

5.TIMED_WAITING

超时等待状态 (TIMED_WAITING) 是线程在指定时间内等待的状态,时间到后线程会自动唤醒。以下方法可以使线程进入超时等待状态:

  • 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):与 parkNanos() 类似,但使用绝对时间戳作为参数。

处于TIMED_WAITING状态的线程被其他线程唤醒或等待的时间到了以后被扭转到RUNNABLE状态。

6. TERMINATED


当线程已完成执行时,处于TERMINATED状态。

已经终止的线程无法再扭转到其它状态


http://www.kler.cn/a/302209.html

相关文章:

  • idea2024创建JavaWeb项目以及配置Tomcat详解
  • 【GO环境安装】mac系统+GoLand使用
  • leetcode hot100除自身以外的数组的乘积
  • 【ArcGIS Pro微课1000例】0064:栅格目录、栅格数据集、镶嵌数据集
  • helm的介绍和安装
  • Ajax中的axios
  • python-pptx在PPT中插入各种形状
  • 【Hadoop|HDFS篇】NameNode和SecondaryNameNode
  • 设计模式学习[5]---装饰模式
  • sqlgun靶场漏洞挖掘
  • 安泰功率放大器有哪些特点呢
  • Linux从入门到开发实战(C/C++)Day13-线程池
  • 滚雪球学SpringCloud[1.1]:Spring Cloud概述与环境搭建(入门章节)
  • QT中使用UTF-8编码
  • Linux echo命令讲解及与重定向符搭配使用方法,tail命令及日志监听方式详解
  • 从戴尔公司中国大饭店DTF大会,看科技外企如何在中国市场发展
  • Docker快速部署Apache Guacamole
  • 前端三件套(HTML,CSS,JS)查漏补缺
  • 交换两实数的整数部分
  • 【数据结构】选择题错题集
  • log4j 的参数配置
  • CUDA-中值滤波算法
  • git标签、repo如何打tag
  • 828华为云征文|基于华为云Flexus云服务器X部署Minio服务
  • 领夹麦克风哪个品牌好?大疆、西圣、博雅无线麦克风在线测评
  • 关于 Embedding 的个人粗略见解