深入解析:Java 线程的“就绪”与“运行”状态究竟有何不同?

引言:多线程世界的迷雾

在我们的日常开发中,多线程编程是一项既强大又充满挑战的技术。它允许我们在同一个程序中并发执行多个任务,从而极大地提高了 CPU 的利用率和程序的响应速度。我们可以将线程想象成进程内部的一个“轻量级进程”,它是 CPU 调度和执行的基本单位。

然而,当你刚开始深入 Java 并发编程时,可能会对线程的生命周期感到困惑。特别是 Runnable(就绪)Running(运行) 这两个状态,虽然听起来很像,但在操作系统层面和 JVM 实现中,它们有着本质的区别。理解这一区别不仅有助于我们通过面试,更能帮助我们在面对线程死锁、饥饿或性能瓶颈时,做出更准确的判断。

在这篇文章中,我们将拨开迷雾,深入探讨这两种状态的定义、区别以及它们在实际代码中的表现。我们将通过多个实战案例,向你展示线程是如何在这两个状态之间切换的,以及如何利用这些知识来优化我们的多线程应用。

线程的生命周期概览

在 Java 中,一个线程在其生命周期中并不是一成不变的。为了管理线程的执行,JVM 定义了几种特定的状态。我们可以通过 Thread.State 枚举来查看这些状态。虽然你在许多图表中可能看到过 5 种甚至 7 种状态的划分,但在 Java 标准库中,主要包含以下六种状态:

  • NEW(新建):线程对象被创建,但 start() 方法尚未被调用。
  • RUNNABLE(就绪/运行):这是本文的重点,表示线程正在 JVM 中执行,或者正在等待操作系统的调度。
  • BLOCKED(阻塞):线程正在等待获取监视器锁,以便进入同步代码块。
  • WAITING(等待):线程在等待另一个线程执行特定动作(如 INLINECODEa2b4626e 或 INLINECODEeef526e4)。
  • TIMEDWAITING(计时等待):线程在等待指定的时间(如 INLINECODE93fca77d)。
  • TERMINATED(终止):线程已经执行完毕。

!Thread Lifecycle

(注:上图展示了线程状态的流转,请注意 Runnable 状态涵盖了“就绪”和“运行”两个阶段。)

核心概念:Runnable 与 Running 的本质区别

很多初学者会将“就绪”和“运行”混为一谈,或者误以为 Java 的 RUNNABLE 状态仅仅代表“正在运行”。让我们来澄清这一点。

什么是 Runnable(就绪状态)?

就绪状态 意味着线程已经准备好执行,它“万事俱备,只欠 CPU”。具体来说:

  • 线程已经进入了运行队列:线程对象已经调用了 start() 方法,并且 JVM 已经将其初始化完毕。
  • 具备执行资格:线程没有处于阻塞、等待或睡眠状态,它拥有所有执行任务所需的资源。
  • 等待 CPU 调度:虽然线程准备好了,但 CPU 同一时刻只能执行一个线程(在单核 CPU 情况下)。因此,它必须等待操作系统的 线程调度器 分配时间片。

我们可以把处于这个状态的线程想象成赛跑运动员站在起跑线上,蓄势待发,只等发令枪响(分配 CPU)。

什么是 Running(运行状态)?

运行状态 是线程生命周期中最活跃的阶段:

  • 正在执行:线程获得了 CPU 资源,正在执行 run() 方法中的代码。
  • 消耗时间片:操作系统会给每个线程分配一个微小的时间片段。只有在这个时间段内,线程才是真正的“运行”。

为什么 Java 没有单独定义“Running”状态?

这是一个非常好的问题。在 Java 的 INLINECODEe85d93fb 枚举中,我们没有看到 INLINECODEbb5cff70,只有 INLINECODEf556d44e。这是因为 Java 将“正在运行”和“准备运行”合并为了 INLINECODEaf6d282d。

  • 从 JVM 的角度看,只要线程是活的,并且没有在等待外部资源(如 IO、锁、睡眠),它就是 RUNNABLE 的。
  • “正在运行”还是“正在排队”是 操作系统线程调度器 的职责,JVM 并不直接控制底层的 CPU 切换。因此,Java 选择了抽象掉这一层细节,统一用 RUNNABLE 表示。

实战演练:观察线程状态的切换

为了更直观地理解这一点,让我们通过代码来看看线程是如何在这些状态之间流转的。

示例 1:多线程的并发执行

在这个经典的例子中,我们将创建三个线程。我们可以观察到,虽然它们几乎同时启动,但执行顺序是混乱的。这是因为线程在 RUNNABLE 队列中竞争 CPU 资源,谁先获得时间片,谁先打印。这就是并发执行的本质。

// Java 程序演示:线程的运行与就绪状态差异

// 导入输入输出类
import java.io.*;

// 辅助类 1
// 继承 Thread 类以定义线程任务
class Thread1 extends Thread {

    // run() 方法是线程的执行体
    public void run() {
        // 线程开始时的提示
        System.out.println("Thread 1 started ");

        // 执行循环任务
        for (int i = 101; i < 200; i++)
            System.out.print(i + " ");

        // 线程结束时的提示
        System.out.println("
Thread 1 completed");
    }
}

// 辅助类 2
class Thread2 extends Thread {

    public void run() {
        System.out.println("Thread 2 started ");

        for (int i = 201; i < 300; i++)
            System.out.print(i + " ");

        System.out.println("
Thread 2 completed");
    }
}

// 辅助类 3
class Thread3 extends Thread {

    public void run() {
        System.out.println("Thread 3 started ");

        for (int i = 301; i < 400; i++)
            System.out.print(i + " ");

        System.out.println("
Thread 3 completed");
    }
}

// 主类
public class ThreadStateDemo {

    public static void main(String[] args) {

        try {
            // 实例化三个线程对象
            // 此时线程处于 NEW 状态
            Thread1 thread1 = new Thread1();
            Thread2 thread2 = new Thread2();
            Thread3 thread3 = new Thread3();

            // 调用 start() 方法
            // 线程从 NEW 转为 RUNNABLE 状态(包含就绪和运行)
            // 它们会竞争 CPU 资源
            thread1.start();
            thread2.start();
            thread3.start();
        }

        catch (Exception e) {
            e.printStackTrace();
        }
    }
}

输出结果分析:

> Thread 1 started

> Thread 2 started

> Thread 3 started

> 301 302 303 … (中间输出省略) … 399

> Thread 3 completed

> 101 102 103 … (中间输出省略) …

发生了什么?

你会注意到输出是交错在一起的。这三个线程几乎同时进入了 RUNNABLE 状态。线程调度器可能在 Thread 3 打印到一半时,暂停它(让出 CPU),转而去执行 Thread 1。这种不可预测的执行顺序正是多线程编程的魅力与挑战所在。

示例 2:使用 Thread.getState() 查看状态

为了让我们“看到”状态,我们可以使用 Java 提供的 getState() 方法。在这个例子中,我们将创建一个线程,在它运行的过程中去检查它的状态。

// 演示如何获取和打印线程状态

public class StateInspector {

    public static void main(String[] args) throws InterruptedException {
        
        // 创建一个任务
        Thread task = new Thread(() -> {
            // 模拟耗时计算
            for (int i = 0; i < 5; i++) {
                System.out.println("Worker thread is working... " + i);
                try { Thread.sleep(500); } catch (Exception e) {}
            }
        });

        // 状态 1: NEW
        System.out.println("Before start: " + task.getState());

        task.start();
        
        // 稍微等待一下,增加主线程捕获到 RUNNABLE 状态的概率
        Thread.sleep(100); 
        
        // 状态 2: RUNNABLE
        // 注意:由于线程调度速度很快,这里可能打印 TIMED_WAITING (因为sleep)
        // 但如果去掉 sleep(500),这里大概率是 RUNNABLE
        System.out.println("During execution: " + task.getState());

        // 等待线程结束
        task.join();
        
        // 状态 3: TERMINATED
        System.out.println("After completion: " + task.getState());
    }
}

这个例子展示了如何动态地监控线程。你会发现,当线程在 INLINECODE372a85e3 内部执行逻辑时,它通常是 INLINECODE0a363583 的;当它调用 INLINECODEe803bdd0 时,它会短暂变为 INLINECODE5679397e,醒来后又回到 RUNNABLE

示例 3:模拟 CPU 抢占

在这个例子中,我们编写一个计算密集型任务。我们将开启两个线程,强制它们一直运行,观察操作系统是如何在它们之间进行切换的。

// 模拟 CPU 密集型任务,展示运行与就绪的切换

class CPUIntensiveTask extends Thread {
    private String name;

    public CPUIntensiveTask(String name) {
        this.name = name;
    }

    @Override
    public void run() {
        long counter = 0;
        // 死循环,占用 CPU
        while (true) {
            counter++;
            // 每隔 100 亿次打印一次
            if (counter % 10_000_000_00L == 0) {
                System.out.println(name + " is calculating... Count: " + counter);
                
                // 在这里,线程可能会因为时间片用尽而停止运行
                // 从 Running 变回 Ready
                // 但从代码角度看,它从未停止执行
            }
        }
    }
}

public class CPUContention {
    public static void main(String[] args) {
        // 开启两个计算密集型线程
        // 如果你的电脑是双核或多核,它们可能会真正并行运行
        // 如果是单核 CPU,它们会频繁地在 Running 和 Ready 之间切换
        
        CPUIntensiveTask t1 = new CPUIntensiveTask("Thread Alpha");
        CPUIntensiveTask t2 = new CPUIntensiveTask("Thread Beta");
        
        t1.start();
        t2.start();
    }
}

常见误区与最佳实践

误区 1:“调用 run() 就是启动线程”

这是一个非常危险的错误。如果你直接调用 INLINECODE488679e2,这只是一个普通的方法调用,并没有启动新的线程。代码会在当前主线程中同步执行。只有调用 INLINECODE552660c3 方法,JVM 才会去调度这个新线程。

误区 2:“线程一旦 start() 就会立即运行”

不一定。当你调用 INLINECODE1aaa4f42 后,线程只是进入了 INLINECODE758458d6 状态。如果系统负载很高,或者有更高优先级的线程,这个线程可能会在“就绪池”中等待很久。

实用建议:如何控制线程的优先级?

虽然我们可以通过 thread.setPriority() 来设置线程的优先级(1-10),但这只是一个建议给操作系统的参数。操作系统并不保证高优先级的线程一定先于低优先级的线程运行。过度依赖优先级来控制业务逻辑是非常危险的。最佳实践是保持默认优先级,通过合理的同步机制和逻辑来控制执行顺序。

总结:关键要点

在这篇文章中,我们深入探讨了 Java 线程的两个核心状态:Runnable(就绪)Running(运行)。让我们回顾一下关键点:

  • 定义不同:INLINECODE8ab65873 表示线程准备好运行并正在等待 CPU 调度;INLINECODE59338d96 表示线程正占用 CPU 执行代码。
  • Java 抽象:Java 在 API 层面将两者统一为 RUNNABLE,具体的切换由底层的操作系统调度器完成。
  • 并发基础:理解这两个状态的切换,是理解多线程抢占、时间片轮转以及高并发程序行为的基础。

多线程编程就像指挥一场交响乐,你是指挥家,但演奏家(线程)何时登场,往往取决于调度器。理解了这些机制,你就能编写出更高效、更稳定的并发程序。

下一步建议:

如果你已经掌握了这些基础,建议接下来研究一下 INLINECODE8f40ff40 方法。这个方法的作用是建议当前线程“让出” CPU,从 INLINECODE9bc96e63 回到 Runnable 状态,给其他线程一个机会。这将是你深入理解线程调度的下一步。

希望这篇文章能帮助你彻底理清这两个概念!如果你有任何疑问,或者在自己的代码中遇到了奇怪的线程行为,欢迎随时交流。

声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理。如需转载,请注明文章出处豆丁博客和来源网址。https://shluqu.cn/22036.html
点赞
0.00 平均评分 (0% 分数) - 0