Chapter 02

线程基础与虚拟线程入门

掌握线程创建、生命周期与核心 API,初探 Java 21 虚拟线程带来的并发革命

什么是线程?

线程(Thread)是程序执行的最小单元,也是 CPU 调度的基本单位。 一个进程(Process)可以包含多个线程,这些线程共享进程的内存空间(堆、方法区), 但每个线程有自己独立的程序计数器(PC)虚拟机栈(Stack)本地方法栈

进程(Process)
操作系统分配资源(内存、文件描述符等)的基本单位。每个进程有独立的内存空间,进程间通信(IPC)成本较高。一个 Java 应用通常是一个进程。
线程(Thread)
CPU 调度的基本单位,在进程内运行。同一进程的线程共享堆内存,线程切换成本远低于进程切换。Java 的平台线程(Platform Thread)直接映射到操作系统线程(1:1 模型)。
虚拟线程(Virtual Thread)
Java 21 正式引入,由 JVM 管理而非操作系统。多个虚拟线程复用少量平台线程(N:M 模型),可以创建数百万个而不耗尽 OS 资源。
协程(Coroutine)
用户态的轻量级并发单元,虚拟线程本质上是 JVM 实现的协程(但对外暴露标准 Thread API)。与 Kotlin 协程、Go goroutine 理念相似。

创建线程的方式

方式一:继承 Thread 类

public class MyThread extends Thread {
    private final String taskName;

    public MyThread(String taskName) {
        super(taskName); // 设置线程名称
        this.taskName = taskName;
    }

    @Override
    public void run() {
        System.out.println("任务 " + taskName + " 在线程 " + getName() + " 中执行");
    }
}

// 使用
Thread t = new MyThread("下载图片");
t.start(); // 启动线程(不能调用 run()!)
start() vs run()

start() 创建新线程并执行 run();直接调用 run() 则在当前线程中同步执行,与普通方法调用无异,不会创建新线程。这是初学者最常犯的错误之一。

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

// Runnable 是函数式接口,可用 lambda 表达式
Runnable task = () -> {
    System.out.println("执行任务,线程:" + Thread.currentThread().getName());
};

Thread t = new Thread(task, "worker-1");
t.start();

// 更简洁的写法
new Thread(() -> System.out.println("Hello from thread"), "my-thread").start();

推荐 Runnable 而非继承 Thread 的原因:Java 是单继承,继承 Thread 就用掉了唯一的父类槽位;而实现接口没有这个限制,更灵活,也更符合「组合优于继承」的设计原则。

方式三:实现 Callable + Future(有返回值)

import java.util.concurrent.*;

Callable<Integer> task = () -> {
    Thread.sleep(1000);
    return 42; // 可以有返回值,Runnable 不行
};

ExecutorService executor = Executors.newSingleThreadExecutor();
Future<Integer> future = executor.submit(task);

// 阻塞等待结果
Integer result = future.get();  // 返回 42
System.out.println("结果:" + result);
executor.shutdown();

线程的生命周期

Java 线程有 6 种状态,定义在 Thread.State 枚举中:

NEW
线程刚被创建,尚未调用 start()。此时线程还没有与 OS 线程关联。
RUNNABLE
线程正在 JVM 中执行(或在等待 CPU 时间片)。注意:RUNNABLE 包含了 OS 层面的「就绪」和「运行中」两种子状态,JVM 不区分。
BLOCKED
线程等待获取一个监视器锁(synchronized),以进入同步代码块或方法。注意:ReentrantLock 等待不是 BLOCKED,而是 WAITING。
WAITING
线程无限期等待另一个线程执行特定操作。调用 Object.wait()、Thread.join()(不带超时)、LockSupport.park() 时进入此状态。
TIMED_WAITING
线程等待指定时间。调用 Thread.sleep(millis)、Object.wait(millis)、Thread.join(millis)、LockSupport.parkNanos() 时进入。
TERMINATED
线程执行完毕(run() 方法正常返回或抛出未捕获异常)。已终止的线程不能再次 start()。
┌─────────────────────────────────┐ │ 线程状态转换图 │ └─────────────────────────────────┘ new Thread() start() ┌───────┐ ┌──────────┐ ┌──────────┐ │ NEW │ ─────────►│ │◄──获得CPU──►│ │ └───────┘ │ RUNNABLE │ │ RUNNING │ (JVM 合并为 RUNNABLE) │ │◄──释放CPU───│ │ └──────────┘ └──────────┘ │ ▲ │ 等待 synchronized │ │ 获得锁 │ wait()/join()/park() 锁(无超时)│ │ ▼ │ ▼ ┌──────────┐ ┌──────────────┐ │ BLOCKED │ │ WAITING │ └──────────┘ │ TIMED_WAITING│ └──────────────┘ │ notify()/interrupt() │ ▼ run() 正常结束或异常 ┌────────────┐ ──────────────────────────► │ TERMINATED │ └────────────┘

核心 Thread API

sleep():让当前线程休眠

// Thread.sleep() —— 使当前线程休眠指定时间
try {
    Thread.sleep(1000); // 休眠 1000 毫秒
} catch (InterruptedException e) {
    Thread.currentThread().interrupt(); // 重新设置中断标志!
    // 处理中断逻辑
}

// Java 9+ 推荐使用 TimeUnit(可读性更好)
TimeUnit.SECONDS.sleep(1);
TimeUnit.MILLISECONDS.sleep(500);

sleep() 会释放 CPU 时间片,但不会释放已持有的锁。 这与 wait()(释放锁)形成对比。

join():等待另一个线程完成

Thread downloader = new Thread(() -> {
    System.out.println("开始下载...");
    try { Thread.sleep(2000); } catch (InterruptedException e) { Thread.currentThread().interrupt(); }
    System.out.println("下载完成!");
});
downloader.start();

System.out.println("等待下载...");
downloader.join();          // 主线程阻塞,直到 downloader 线程终止
System.out.println("所有任务完成");

// join(timeout) 版本:最多等待指定毫秒数
downloader.join(3000); // 最多等 3 秒

interrupt():中断线程

Java 线程中断采用协作式中断模型:调用 interrupt() 只是 设置目标线程的中断标志位,并不强制终止线程。被中断的线程需要自己检查 中断状态并决定如何响应。

Thread worker = new Thread(() -> {
    while (!Thread.currentThread().isInterrupted()) { // 主动检查中断标志
        try {
            System.out.println("工作中...");
            Thread.sleep(500); // 阻塞时收到中断 → 抛出 InterruptedException
        } catch (InterruptedException e) {
            System.out.println("收到中断信号,清理后退出");
            Thread.currentThread().interrupt(); // 重新标记中断(捕获后中断标志被清除)
            break;
        }
    }
    System.out.println("线程正常退出");
});
worker.start();
Thread.sleep(1500);
worker.interrupt(); // 请求中断
禁止使用 stop() 和 suspend()

Thread.stop() 会立即终止线程并强制释放所有锁,可能导致数据结构处于不一致状态。 Thread.suspend()/resume() 容易导致死锁。 两者均已标记为 @Deprecated,请永远不要使用。 正确的做法是使用协作式中断(interrupt() + 主动检查)。

线程优先级与守护线程

Thread t = new Thread(() -> System.out.println("task"));

// 线程优先级(1-10,默认5)
t.setPriority(Thread.MAX_PRIORITY); // 10
t.setPriority(Thread.MIN_PRIORITY); // 1
t.setPriority(Thread.NORM_PRIORITY); // 5(默认)
// 注意:优先级只是"建议",实际调度由 OS 决定,不同平台行为差异大

// 守护线程(Daemon Thread)
// 守护线程在所有非守护线程结束后自动退出(JVM 随之退出)
Thread daemon = new Thread(() -> {
    while (true) {
        // 后台监控任务
    }
});
daemon.setDaemon(true); // 必须在 start() 之前设置
daemon.start();

ThreadLocal:线程本地变量

ThreadLocal 为每个线程提供独立的变量副本,彻底消除共享,从根本上避免并发冲突。 常见用途:数据库连接、用户会话信息、事务上下文传递。

public class UserContext {
    // 每个线程都有自己独立的 userId 副本
    private static final ThreadLocal<String> USER_ID =
        ThreadLocal.withInitial(() -> "anonymous");

    public static void setUserId(String id) { USER_ID.set(id); }
    public static String getUserId() { return USER_ID.get(); }

    // 非常重要:用完必须清除,防止线程池中的线程复用导致数据污染
    public static void clear() { USER_ID.remove(); }
}

// Web 请求处理示例(Filter 中设置,Controller 中使用)
UserContext.setUserId("user-123");
try {
    processRequest(); // 整个调用链都能通过 UserContext.getUserId() 获取
} finally {
    UserContext.clear(); // 必须在 finally 中清除!
}
ThreadLocal 内存泄漏

在线程池环境中,线程会被复用。如果没有调用 remove() 清除 ThreadLocal 值, 下一个使用该线程的请求可能读到上一个请求残留的数据。更严重的是,若 ThreadLocal key 的强引用 被回收后,value 仍挂在 ThreadLocalMap 中无法 GC,造成内存泄漏。 规范:每次使用完 ThreadLocal 务必在 finally 块中调用 remove()。

Java 21 虚拟线程(Virtual Threads)入门

平台线程的瓶颈

传统的 Java 平台线程(Platform Thread)与操作系统线程 1:1 对应。创建一个平台线程需要在 OS 层 分配约 1MB 的栈空间,线程切换涉及内核态/用户态切换,开销较大。 通常一个 JVM 进程最多支持数千个平台线程——这在处理大量 I/O 密集型并发请求时会成为瓶颈。

传统解法是使用异步/响应式编程(Reactive Programming),但这带来了「回调地狱」或复杂的 API, 破坏了代码的线性可读性。

虚拟线程的解决方案

Java 21(JEP 444)正式发布虚拟线程(Virtual Threads),由 JVM 管理, 运行在一小批平台线程(称为载体线程 Carrier Thread)之上。 虚拟线程在等待 I/O 时会自动卸载(unmount)离开载体线程, 让载体线程去执行其他虚拟线程;I/O 就绪后再重新挂载(mount)继续执行。

平台线程模型(传统): OS Thread 1 ──► Platform Thread ──► 阻塞 I/O → 等待(OS 线程空闲浪费) OS Thread 2 ──► Platform Thread ──► 阻塞 I/O → 等待 OS Thread 3 ──► Platform Thread ──► 阻塞 I/O → 等待 ...(最多数千个) 虚拟线程模型(Java 21): OS Thread 1 ──► Carrier Thread 1 ──► VThread A(执行) ──► VThread B(卸载,等 I/O) ──► VThread C(卸载,等 I/O) OS Thread 2 ──► Carrier Thread 2 ──► VThread D(执行) ...(少量 Carrier,承载百万 VThread)

创建虚拟线程

// 方式一:Thread.ofVirtual()(推荐)
Thread vt = Thread.ofVirtual()
    .name("vthread-1")
    .start(() -> System.out.println("虚拟线程执行"));

// 方式二:工厂方法
Thread.Builder.OfVirtual builder = Thread.ofVirtual().name("task-", 0);
Thread vt2 = builder.start(() -> {
    System.out.println("任务2,线程:" + Thread.currentThread());
});

// 方式三:使用虚拟线程执行器(适合提交大量任务)
try (ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor()) {
    for (int i = 0; i < 1_000_000; i++) {
        final int taskId = i;
        executor.submit(() -> {
            // 模拟 I/O 操作(虚拟线程在此处卸载,不阻塞载体线程)
            Thread.sleep(Duration.ofMillis(100));
            return "task-" + taskId;
        });
    }
} // try-with-resources 自动关闭并等待所有任务完成

检测当前线程是否为虚拟线程

Thread t = Thread.currentThread();
System.out.println("是否为虚拟线程:" + t.isVirtual()); // Java 21 新方法
System.out.println("线程名:" + t.getName());
System.out.println("线程描述:" + t); // VirtualThread[#21]/runnable@ForkJoinPool...

虚拟线程的关键特性

优势

  • 创建成本极低(约几KB内存 vs 平台线程1MB)
  • 可创建百万级虚拟线程
  • 保持同步代码的可读性(无需 async/await)
  • 完全兼容现有 Thread API
  • I/O 阻塞时自动挂起,不浪费 OS 线程

注意事项

  • CPU 密集型任务不适合(不能改善 CPU 利用率)
  • synchronized 块会导致 pinning(固定载体线程)
  • 不应使用线程池管理虚拟线程(每任务一虚拟线程)
  • ThreadLocal 仍可用,但注意内存(百万线程×ThreadLocal)
  • 栈变量仍在堆中,GC 压力可能增加
// 性能对比演示:1万个并发 HTTP 请求
import java.net.http.*;
import java.net.*;

// 传统方式:需要维护连接池,限制并发数
ExecutorService pool = Executors.newFixedThreadPool(200); // 最多200个并发

// 虚拟线程方式:每请求一虚拟线程,轻松支持1万并发
HttpClient client = HttpClient.newBuilder()
    .executor(Executors.newVirtualThreadPerTaskExecutor())
    .build();

try (ExecutorService vPool = Executors.newVirtualThreadPerTaskExecutor()) {
    for (int i = 0; i < 10_000; i++) {
        vPool.submit(() -> {
            HttpRequest req = HttpRequest.newBuilder()
                .uri(URI.create("https://api.example.com/data"))
                .build();
            return client.send(req, HttpResponse.BodyHandlers.ofString());
        });
    }
}
虚拟线程最佳实践

虚拟线程专为 I/O 密集型任务设计(数据库查询、HTTP 调用、文件读写等)。 不要用线程池来「管理」虚拟线程(失去了虚拟线程的轻量级优势),直接使用 Executors.newVirtualThreadPerTaskExecutor()Thread.ofVirtual().start()。 CPU 密集型任务仍应使用平台线程或 ForkJoinPool。

本章小结