引言:多线程世界的迷雾
在我们的日常开发中,多线程编程是一项既强大又充满挑战的技术。它允许我们在同一个程序中并发执行多个任务,从而极大地提高了 CPU 的利用率和程序的响应速度。我们可以将线程想象成进程内部的一个“轻量级进程”,它是 CPU 调度和执行的基本单位。
然而,当你刚开始深入 Java 并发编程时,可能会对线程的生命周期感到困惑。特别是 Runnable(就绪) 和 Running(运行) 这两个状态,虽然听起来很像,但在操作系统层面和 JVM 实现中,它们有着本质的区别。理解这一区别不仅有助于我们通过面试,更能帮助我们在面对线程死锁、饥饿或性能瓶颈时,做出更准确的判断。
在这篇文章中,我们将拨开迷雾,深入探讨这两种状态的定义、区别以及它们在实际代码中的表现。我们将通过多个实战案例,向你展示线程是如何在这两个状态之间切换的,以及如何利用这些知识来优化我们的多线程应用。
线程的生命周期概览
在 Java 中,一个线程在其生命周期中并不是一成不变的。为了管理线程的执行,JVM 定义了几种特定的状态。我们可以通过 Thread.State 枚举来查看这些状态。虽然你在许多图表中可能看到过 5 种甚至 7 种状态的划分,但在 Java 标准库中,主要包含以下六种状态:
- NEW(新建):线程对象被创建,但
start()方法尚未被调用。 - RUNNABLE(就绪/运行):这是本文的重点,表示线程正在 JVM 中执行,或者正在等待操作系统的调度。
- BLOCKED(阻塞):线程正在等待获取监视器锁,以便进入同步代码块。
- WAITING(等待):线程在等待另一个线程执行特定动作(如 INLINECODEa2b4626e 或 INLINECODEeef526e4)。
- TIMEDWAITING(计时等待):线程在等待指定的时间(如 INLINECODE93fca77d)。
- TERMINATED(终止):线程已经执行完毕。
(注:上图展示了线程状态的流转,请注意 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 状态,给其他线程一个机会。这将是你深入理解线程调度的下一步。
希望这篇文章能帮助你彻底理清这两个概念!如果你有任何疑问,或者在自己的代码中遇到了奇怪的线程行为,欢迎随时交流。