从单线程到多线程:Java Thread 的前世今生与状态流转

5 阅读8分钟

从单线程到多线程:Java Thread 的前世今生与状态流转

前言:为什么我们需要多线程?

想象一下,你是一个餐厅服务员,同时有三桌客人需要点餐、上菜、结账。如果你只能按顺序一桌一桌服务,那等待的客人肯定会抱怨"太慢了"。多线程就像雇佣了多个服务员,每个人负责自己的工作,效率瞬间提升。

但在享受高效的同时,你也需要面对新的问题:多个服务员会不会撞到一起?谁先服务谁?服务完怎么汇报?这些正是 Java 多线程要解决的核心问题。

今天,我们就来深入剖析 Java 线程的"一生"——从诞生到消亡的完整旅程,以及三种创建线程的方式,带你真正搞懂多线程的本质。

第一幕:线程的六种状态——一个线程的一生

Java 通过 Thread.State 枚举精确定义了线程的 6 种状态,每个状态都代表了线程在某个特定时刻的处境:

状态含义生活类比
NEW线程对象已创建,但未启动新员工入职报道,还没开始工作
RUNNABLE线程正在运行或等待 CPU员工在工位上忙碌(或排队等电脑用)
BLOCKED线程因竞争锁被阻塞员工想用打印机,但别人正在用,只能等
WAITING线程进入无超时等待员工在会议室等人开会,不设结束时间
TIMED_WAITING线程进入有超时等待员工设置了 30 分钟闹钟小憩
TERMINATED线程执行完毕或异常终止员工下班或离职了

状态流转全景图

plaintext

NEW → RUNNABLE → [BLOCKED / WAITING / TIMED_WAITING] → TERMINATED

从 NEW 到 TERMINATED,一个线程完整走完它的一生,期间可能在 BLOCKED、WAITING、TIMED_WAITING 之间反复切换——这就是线程的"职场生涯"。

第二幕:Thread 核心 API 五虎将

理解了状态流转,我们来看看那些控制线程命运的五个核心方法。

1️⃣ start() vs run() —— 启动 vs 执行

这是新手最容易混淆的两个方法:

  • run() :只是 Thread 类的一个普通方法,直接调用不会启动新线程,相当于在当前线程"自己干活"。
  • start() :真正的"新线程启动器",JVM 会分配资源并调用 run(),让线程进入 RUNNABLE 状态。

java

public class ThreadStartRunDemo {
    public static void main(String[] args) {
        Thread thread = new Thread(() -> {
            System.out.println("当前执行线程名:" + Thread.currentThread().getName());
            System.out.println("线程状态:" + Thread.currentThread().getState());
        }, "MyThread");

        // 调用 start 前,线程只是个"新生儿"
        System.out.println("调用start前,线程状态:" + thread.getState()); // NEW

        // 直接调用 run():不会启动新线程,在 main 线程中执行
        System.out.println("\n=== 直接调用run() ===");
        thread.run(); // 输出:当前执行线程名:main

        // 调用 start():真正启动新线程
        System.out.println("\n=== 调用start() ===");
        thread.start(); // 输出:当前执行线程名:MyThread,状态:RUNNABLE

        // 等待线程执行完毕
        Thread.sleep(100);
        System.out.println("线程执行完毕后状态:" + thread.getState()); // TERMINATED
    }
}

2️⃣ join() —— 我要等你做完

join() 让当前线程"暂停等待",直到目标线程执行完毕。就像老板说:"我先等你做完这个项目,再安排下一个任务。"

java

public class ThreadJoinDemo {
    public static void main(String[] args) throws InterruptedException {
        Thread taskThread = new Thread(() -> {
            for (int i = 0; i < 3; i++) {
                try {
                    Thread.sleep(500); // 模拟耗时操作
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("子线程执行中:" + i);
            }
        }, "TaskThread");

        taskThread.start();
        System.out.println("main线程等待子线程执行...");

        taskThread.join(); // main 线程进入 WAITING 状态,等 taskThread 执行完

        System.out.println("子线程执行完毕,main线程继续执行");
    }
}

输出结果

plaintext

main线程等待子线程执行...
子线程执行中:0
子线程执行中:1
子线程执行中:2
子线程执行完毕,main线程继续执行

3️⃣ interrupt() —— 礼貌地请求中断

注意!interrupt() 不是"强制杀掉线程",而是"贴个标签说请停止":

  • 给线程设置中断标记(isInterrupted() 返回 true);
  • 如果线程处于阻塞状态(sleep、join、wait),会抛出 InterruptedException 并清除标记;
  • 线程需要自己检测标记,决定是否停止——这是"优雅中断"的精髓。

4️⃣ isAlive() —— 你还在吗?

isAlive() 返回线程是否"活着"(非 NEW 且非 TERMINATED)。

java

public class ThreadInterruptIsAliveDemo {
    public static void main(String[] args) throws InterruptedException {
        Thread worker = new Thread(() -> {
            // 循环检测中断标记,决定是否退出
            while (!Thread.currentThread().isInterrupted()) {
                System.out.println("工作线程运行中...");
                try {
                    Thread.sleep(1000); // 进入 TIMED_WAITING
                } catch (InterruptedException e) {
                    System.out.println("工作线程被中断,清除中断标记");
                    // 抛出异常后中断标记会被清除,需要手动再次中断
                    Thread.currentThread().interrupt();
                }
            }
            System.out.println("工作线程退出,中断标记:" + Thread.currentThread().isInterrupted());
        }, "WorkerThread");

        // 启动前,isAlive() = false
        System.out.println("启动前,isAlive:" + worker.isAlive()); // false

        worker.start();
        System.out.println("启动后,isAlive:" + worker.isAlive()); // true

        // 主线程等待 3 秒后中断工作线程
        Thread.sleep(3000);
        worker.interrupt();

        // 等待工作线程终止
        worker.join();
        System.out.println("终止后,isAlive:" + worker.isAlive()); // false
    }
}

5️⃣ 完整状态流转示例

下面这个例子展示了线程从 NEW → RUNNABLE → TIMED_WAITING → TERMINATED 的完整旅程:

java

public class ThreadStateDemo {
    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(() -> {
            try {
                // 线程运行中,状态为 RUNNABLE
                System.out.println("线程执行中,状态:" + Thread.currentThread().getState());
                // 调用 sleep,进入 TIMED_WAITING
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            // 执行完毕,进入 TERMINATED
        }, "StateDemoThread");

        // 1. NEW 状态:创建但未启动
        System.out.println("1. 初始状态:" + thread.getState()); // NEW

        // 2. 调用 start(),进入 RUNNABLE
        thread.start();
        Thread.sleep(100); // 等线程启动
        System.out.println("2. 启动后状态:" + thread.getState()); // RUNNABLE/TIMED_WAITING

        // 3. 线程 sleep 中,状态为 TIMED_WAITING
        Thread.sleep(500);
        System.out.println("3. sleep中状态:" + thread.getState()); // TIMED_WAITING

        // 4. 等待线程执行完,状态为 TERMINATED
        thread.join();
        System.out.println("4. 执行完毕状态:" + thread.getState()); // TERMINATED
    }
}

第三幕:三种创建线程的方式——你适合哪一种?

先明确一个核心误区

实现 Runnable/Callable 本身并不是"创建线程",而是定义"线程要执行的任务";真正创建并启动线程的始终是 Thread 类。

方式一:继承 Thread 类(不推荐)

通过继承 Thread 类,重写 run() 方法。这种方式简单直观,但缺点明显:由于 Java 单继承限制,继承 Thread 后无法再继承其他类,线程和任务高度耦合。

java

class MyThread extends Thread {
    @Override
    public void run() {
        System.out.println("继承Thread的线程执行:" + Thread.currentThread().getName());
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

public class ThreadExtendDemo {
    public static void main(String[] args) {
        MyThread thread1 = new MyThread();
        MyThread thread2 = new MyThread();
        thread1.start();
        thread2.start();
    }
}

方式二:实现 Runnable 接口(推荐)

Runnable 是函数式接口,定义任务逻辑后,传入 Thread 构造器来创建线程。任务和线程解耦,多个线程可以共享同一个任务

java

class MyRunnable implements Runnable {
    @Override
    public void run() {
        System.out.println("实现Runnable的线程执行:" + Thread.currentThread().getName());
    }
}

public class RunnableImplDemo {
    public static void main(String[] args) {
        MyRunnable task = new MyRunnable();
        Thread thread1 = new Thread(task, "Runnable线程1");
        Thread thread2 = new Thread(task, "Runnable线程2");
        thread1.start();
        thread2.start();

        // Lambda 简化写法
        Thread thread3 = new Thread(() -> {
            System.out.println("Lambda简化Runnable:" + Thread.currentThread().getName());
        }, "Lambda线程");
        thread3.start();
    }
}

共享任务示例(多线程卖票):

java

Runnable ticketTask = () -> {
    for (int i = 1; i <= 10; i++) {
        System.out.println(Thread.currentThread().getName() + "卖出第" + i + "张票");
    }
};

// 3 个线程卖同一批票
new Thread(ticketTask, "窗口1").start();
new Thread(ticketTask, "窗口2").start();
new Thread(ticketTask, "窗口3").start();

方式三:实现 Callable 接口(需要返回值时)

Callable 是 JDK 1.5 新增的接口,解决了 Runnable 无返回值、不能抛检查异常的问题,需要配合 FutureTask 来获取返回值。

java

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

class MyCallable implements Callable<Integer> {
    private int num;

    public MyCallable(int num) {
        this.num = num;
    }

    @Override
    public Integer call() throws Exception {
        int sum = 0;
        for (int i = 1; i <= num; i++) {
            sum += i;
            Thread.sleep(10); // 可直接 throws,无需 try-catch
        }
        return sum;
    }
}

public class CallableImplDemo {
    public static void main(String[] args) {
        Callable<Integer> callableTask = new MyCallable(100);
        FutureTask<Integer> futureTask = new FutureTask<>(callableTask);
        Thread thread = new Thread(futureTask, "Callable线程");
        thread.start();

        try {
            Integer result = futureTask.get(); // 阻塞等待结果
            System.out.println("1-100的和:" + result); // 输出 5050
        } catch (InterruptedException | ExecutionException e) {
            e.printStackTrace();
        }
    }
}

三种方式对比表

特性继承 Thread 类实现 Runnable 接口实现 Callable 接口
核心方法run()(无返回值)run()(无返回值)call()(有返回值)
异常处理只能 try-catch只能 try-catch可 throws 检查异常
线程-任务耦合度高(线程 = 任务)低(任务独立)低(任务独立)
继承限制不能继承其他类可继承其他类可继承其他类
返回值有(通过 FutureTask)
适用场景简单任务、无需复用任务复用、多线程共享需要返回结果 / 抛异常

总结:选择策略

  1. 简单无返回值:优先用 Runnable(配合 Lambda 简化代码);
  2. 需要返回值 / 抛异常:用 Callable + FutureTask
  3. 尽量避免继承 Thread(单继承限制 + 耦合度高);
  4. 实际开发:很少直接 new Thread,而是用 ExecutorService 线程池管理 Runnable/Callable 任务,更高效。

进阶思考

当你掌握了线程的基础知识和创建方式后,下一步应该思考:

  • 线程安全:多个线程共享数据时,如何避免数据竞争?(synchronized、Lock、volatile)
  • 线程池:频繁创建销毁线程开销大,如何复用线程?(ExecutorService、ThreadPoolExecutor)
  • 并发工具:JUC 包提供了哪些强大的并发工具?(CountDownLatch、CyclicBarrier、Semaphore)

多线程的世界博大精深,今天我们只是揭开了序幕。接下来,让我们一起继续探索并发编程的更多奥秘吧!

推荐阅读

  • 《Java 并发编程实战》—— 并发领域的圣经
  • JDK 源码:java.lang.Threadjava.util.concurrent

互动话题:你在项目中遇到过哪些多线程相关的坑?欢迎在评论区分享你的经历!