Skip to content

进程、线程、协程:并发三剑客的本质区别

凌晨2点,你的Java服务突然CPU飙升80%。你抓了线程dump,发现一堆线程都在等锁。 你开始怀疑人生:不是说多线程能提升性能吗?怎么反而更慢了?

要回答这个问题,得先搞清楚三个基本概念:进程、线程、协程

进程:资源的容器

进程是资源分配的基本单位。当你启动一个Java程序,操作系统会创建一个进程,为它分配独立的内存空间、文件句柄、信号量等资源。

java
public class ProcessDemo {
    public static void main(String[] args) {
        // 每个运行中的 Java 程序都是一个独立的进程
        System.out.println("进程ID: " + ProcessHandle.current().pid());
        // 在 Linux 上,可以用 ps aux | grep java 查看这个进程
    }
}

进程的特点:

  • 拥有独立的地址空间(进程A崩溃不会影响进程B)
  • 进程间通信需要额外的机制(IPC)
  • 创建和切换开销大(需要切换独立的内存空间)

你有没有想过,为什么JVM崩溃了,你的IDE还能正常运行?因为它们是不同的进程。

线程:调度的单位

线程是CPU调度的基本单位。一个进程可以包含多个线程,这些线程共享进程的地址空间和资源。

java
public class ThreadDemo {
    public static void main(String[] args) {
        // main 方法运行在 main 线程中
        System.out.println("主线程: " + Thread.currentThread().getName());

        // 创建新线程
        Thread worker = new Thread(() -> {
            // 这个代码运行在新线程中
            System.out.println("工作线程: " + Thread.currentThread().getName());
        });
        worker.start();
    }
}

线程 vs 进程的关键区别:

特性进程线程
资源分配独立地址空间共享进程资源
开销创建/切换开销大创建/切换开销小
通信需要IPC机制直接读写共享内存
隔离性进程间隔离线程间共享(注意线程安全)
java
// 线程共享进程的哪些资源?
public class SharedResources {
    private int count = 0;  // 实例变量 - 线程间共享(注意线程安全!)

    public void increment() {
        count++;  // 多个线程同时操作,可能出问题
    }

    // 局部变量在线程栈中,每个线程有自己的副本
    public void process() {
        int localVar = 0;  // 线程私有
        // ...
    }
}

协程又是什么?为什么最近这么火?

协程:用户态的轻量级线程

协程(Coroutine)是用户态的轻量级执行单元。与线程由操作系统调度不同,协程由程序自身控制切换。

java
// Java 21 虚拟线程示例
public class VirtualThreadDemo {
    public static void main(String[] args) throws InterruptedException {
        // 创建10000个虚拟线程
        try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
            IntStream.range(0, 10000).forEach(i -> {
                executor.submit(() -> {
                    // 虚拟线程非常轻量,可以创建海量
                    System.out.println("虚拟线程 " + i);
                    return i;
                });
            });
        }
    }
}

协程 vs 线程的本质区别:

特性线程协程
调度者操作系统内核用户态程序
切换开销涉及内核态切换用户态切换,开销极小
阻塞影响阻塞线程会导致内核调度阻塞时主动让出,不影响其他协程
数量几千个就很多了可以轻松创建上百万个
java
// 协程的「让出」机制 - 模拟
public class CoroutineDemo {
    public static void main(String[] args) {
        // 伪代码,展示协程的工作方式
        coroutineA = new Coroutine(() -> {
            for (int i = 0; i < 1000; i++) {
                if (i == 500) {
                    yield();  // 主动让出CPU,切换到其他协程
                }
            }
        });

        coroutineB = new Coroutine(() -> {
            // 可以和 coroutineA 交替执行
        });

        scheduler.run();  // 用户态调度器管理协程切换
    }
}

Java中的线程模型演进

Java的线程模型经历了几个阶段:

1. 平台线程(1.0 - 20)

传统的 java.lang.Thread,直接映射到操作系统的内核线程。

java
// 传统线程模型 - 每个线程对应一个内核线程
Thread thread = new Thread(() -> {
    // 阻塞操作会阻塞整个内核线程
    try {
        Thread.sleep(1000);  // 线程阻塞,但这是OS级别的阻塞
    } catch (InterruptedException e) {}
});
thread.start();

问题:当你用Tomcat处理10000个并发请求时,需要创建10000个线程。线程的栈空间默认1MB,光栈就要10GB内存。

2. 线程池(Java 5+)

通过复用线程减少创建开销,但本质还是一对一映射。

java
// 线程池 - 复用线程,减少创建开销
ExecutorService pool = Executors.newFixedThreadPool(100);
// 100个线程处理10000个请求
for (int i = 0; i < 10000; i++) {
    pool.submit(() -> handleRequest(i));
}

问题:每个请求对应一个线程,线程是稀缺资源,上限受限于内存和CPU。

3. 虚拟线程(Java 21+)

里程碑式的改变——虚拟线程由JVM管理,不直接映射内核线程。

java
// 虚拟线程 - 百万级别的并发不再是问题
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
    IntStream.range(0, 1_000_000).forEach(i -> {
        executor.submit(() -> {
            // 100万个虚拟线程轻松创建!
            // 它们在背后复用少量内核线程
            return i;
        });
    });
}

虚拟线程的原理:

用户视角:  [V1] [V2] [V3] ... [V1000000]  (100万个虚拟线程)
           ↓     ↓     ↓
JVM调度器:  [Carrier Thread Pool] (可能只有几百个内核线程)
           ↓     ↓     ↓
操作系统:   [K1] [K2] [K3] ... [Kn] (实际的内核线程)

实战:选择正确的并发模型

java
public class ConcurrencyChoice {
    public static void main(String[] args) {
        // CPU密集型任务 - 用线程池,线程数 = CPU核心数
        int cpuCores = Runtime.getRuntime().availableProcessors();
        ExecutorService cpuPool = Executors.newFixedThreadPool(cpuCores);

        // IO密集型任务 - 虚拟线程更适合
        // 因为大部分时间在等待IO,协程可以让出CPU
        try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
            for (int i = 0; i < 10000; i++) {
                executor.submit(() -> {
                    // 模拟HTTP请求、数据库查询等IO操作
                    return fetchData();
                });
            }
        }
    }
}

总结:一张图说清楚

┌─────────────────────────────────────────────────────────┐
│                        进程                               │
│  ┌─────────────────────────────────────────────────┐    │
│  │                    线程                          │    │
│  │  ┌─────────┐  ┌─────────┐  ┌─────────┐        │    │
│  │  │ 协程1   │  │ 协程2   │  │ 协程3   │  ...   │    │
│  │  └─────────┘  └─────────┘  └─────────┘        │    │
│  └─────────────────────────────────────────────────┘    │
└─────────────────────────────────────────────────────────┘

资源隔离:   进程 > 线程 > 协程
切换开销:   进程 > 线程 > 协程
编程复杂度: 进程 > 线程 > 协程

面试追问方向

  • 虚拟线程和传统线程的区别是什么?底层原理是什么? 提示:JVM如何实现虚拟线程的挂起和恢复?Carrier Thread是什么?
  • 协程在Java中怎么实现的?除了Java 21的虚拟线程,还有哪些方案? 提示:Quasar、Kotlin协程、Loom项目。
  • 为什么Redis/Nginx用单线程模型却能高性能? 提示:IO多路复用 + 非阻塞IO + 事件驱动。
  • 既然协程这么好,为什么JDK 21才引入虚拟线程?之前这么多年Java程序员是怎么解决的? 提示:异步编程、CompletableFuture、响应式框架(Reactor)。

基于 VitePress 构建