🌺The Begin🌺点点关注,收藏不迷路🌺

1. 引言:从“线程太重”说起

在传统的 Java 并发编程中,我们经常面临一个尴尬的局面:为了处理 I/O 密集型任务(如数据库查询、HTTP 请求),我们不得不创建大量线程。但每个线程都要消耗 1MB 左右的栈内存,而且线程切换依赖操作系统调度,成本高昂。这就导致了一个问题——C10K 问题(处理 1 万个并发连接)在传统线程模型下举步维艰。

那么,有没有一种更轻量的并发模型呢?答案是:协程

本文将深入浅出地解释什么是协程,对比它与线程的异同,并重点分析 Java 生态中协程支持的现状与未来。

2. 一图看懂:协程是什么?

协程/虚拟线程模型 (M:N)

虚拟线程1

平台线程1

虚拟线程2

虚拟线程3

平台线程2

虚拟线程4

操作系统线程1

操作系统线程2

CPU核心

协程切换:用户态极轻量

传统线程模型 (1:1)

Java线程1

操作系统线程1

Java线程2

操作系统线程2

Java线程3

操作系统线程3

CPU核心

线程切换:内核态开销大

核心区别

  • 线程:内核态资源,创建/切换成本高,数量受限
  • 协程:用户态资源,创建/切换成本极低,可达百万级

3. 协程的核心概念

3.1 什么是协程?

协程(Coroutine)是一种可以在执行过程中主动暂停(suspend)并在之后恢复(resume)的函数或任务。它与线程的关键区别在于:

特性 线程 协程
调度方式 操作系统抢占式调度 用户态协作式调度
创建开销 高(毫秒级) 极低(纳秒级)
栈内存 约 1MB 约 数百字节 ~ KB级
切换成本 微秒级(内核态) 纳秒级(用户态)
最大数量 数千 ~ 数万 百万级
适用场景 CPU 密集型 I/O 密集型、高并发

线程的生命周期

时间片用完/阻塞

CPU调度

完成

创建

运行

就绪/等待

结束

协程的生命周期

主动让出

恢复

完成

创建

运行

挂起 Suspend

结束

3.2 协程如何解决“卡顿”问题?

在 GUI 程序或 Web 服务器中,卡顿通常源于线程被阻塞。协程的解决思路是:当遇到 I/O 操作时,主动让出 CPU,让其他协程执行

协程2 调度器 协程1 协程2 调度器 协程1 执行业务逻辑 切换 执行业务逻辑 I/O完成,唤醒协程1 从挂起点继续 遇到I/O操作,主动挂起 调度协程2执行 挂起/完成 恢复执行

对比传统的阻塞线程:传统线程在 I/O 时会阻塞,操作系统必须切换执行其他线程(成本高);而协程只需在用户态保存少量上下文,切换成本极低。

4. Java 协程的探索之路

4.1 历史方案:Quasar

在 Oracle 官方解决方案成熟之前,Java 生态中出现了第三方的协程库,Quasar 是其中较有代表性的一个。

Quasar 通过字节码增强技术,在 Java 中实现了纤程(Fiber),代码示例如下:

import co.paralleluniverse.fibers.Fiber;
import co.paralleluniverse.strands.channels.Channels;
import co.paralleluniverse.strands.channels.IntChannel;

public class QuasarExample {
    public static void main(String[] args) throws Exception {
        IntChannel channel = Channels.newIntChannel(0);
        
        // 生产者纤程
        new Fiber(() -> {
            for (int i = 0; i < 5; i++) {
                channel.send(i);
                System.out.println("Sent: " + i);
            }
            channel.close();
        }).start();
        
        // 消费者纤程
        new Fiber(() -> {
            while (!channel.isClosed()) {
                int received = channel.receive();
                System.out.println("Received: " + received);
            }
        }).start();
    }
}

Quasar 的局限性

  • 需要特殊的 Java Agent 或编译器支持
  • 社区活跃度下降,逐渐被官方方案取代

4.2 官方方案:Project Loom 与虚拟线程

Oracle 的 Project Loom 是 Java 官方的协程解决方案,经过多年酝酿,终于在 Java 21(2023 年 9 月)中正式推出虚拟线程(Virtual Threads)。

虚拟线程的核心特征:

特征 说明
轻量级 每个虚拟线程仅占用几百字节内存
M:N 调度 大量虚拟线程运行在少量平台线程上
透明阻塞 I/O 操作自动让出载体线程
兼容性强 与现有 Thread API 高度兼容
结构化并发 通过 StructuredTaskScope 管理任务生命周期

4.3 虚拟线程的核心原理:Continuation

虚拟线程的实现依赖于 JVM 内部引入的 Continuation(续体) 机制。

虚拟线程阻塞处理流程

虚拟线程执行 I/O

JVM 识别阻塞点

保存执行上下文
封装为 Continuation

从载体线程卸载

载体线程释放,执行其他任务

I/O 完成,OS 通知 JVM

Continuation 重新激活

挂载到空闲载体线程

从断点恢复执行

关键点:整个过程对开发者透明——你写的代码看起来是同步阻塞的,但底层 JVM 已经将其转换为非阻塞执行。

5. Java 虚拟线程实战指南

5.1 创建虚拟线程的两种方式

// 方式一:Thread.ofVirtual() 工厂方法
Thread vThread = Thread.ofVirtual()
    .name("my-virtual-thread")
    .start(() -> {
        System.out.println("Hello from virtual thread!");
        // 模拟阻塞 I/O,虚拟线程会自动让出载体线程
        Thread.sleep(Duration.ofSeconds(1));
    });

vThread.join();  // 等待完成

// 方式二:Executors.newVirtualThreadPerTaskExecutor()(推荐)
try (ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor()) {
    // 提交 10,000 个任务,每个任务都会创建新的虚拟线程
    IntStream.range(0, 10000).forEach(i -> {
        executor.submit(() -> {
            Thread.sleep(Duration.ofMillis(100));
            return i;
        });
    });
}  // 自动等待所有任务完成

5.2 结构化并发:管理多个并发任务

Java 21 引入了 StructuredTaskScope,用于管理多个虚拟线程的生命周期:

// 使用结构化并发同时调用多个服务
UserInfo fetchUserInfo(String userId) throws Exception {
    try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
        // 并发执行两个任务
        Future<User> userFuture = scope.fork(() -> fetchUser(userId));
        Future<Orders> ordersFuture = scope.fork(() -> fetchOrders(userId));
        
        scope.join();           // 等待所有任务完成(或任一失败)
        scope.throwIfFailed();  // 若有失败则抛出异常
        
        // 获取结果
        return new UserInfo(userFuture.resultNow(), ordersFuture.resultNow());
    }
}

5.3 避坑指南

虚拟线程虽然强大,但有两个关键陷阱需要注意:

陷阱一:synchronized 导致“钉住”(Pinning)

// ❌ 错误:synchronized 块内的 I/O 会钉住载体线程
synchronized(lock) {
    Thread.sleep(1000);  // 虚拟线程无法让出载体线程!
}

// ✅ 正确:改用 ReentrantLock
private final Lock lock = new ReentrantLock();
lock.lock();
try {
    Thread.sleep(1000);  // 正常让出
} finally {
    lock.unlock();
}

陷阱二:ThreadLocal 的内存问题

// ❌ 创建百万个虚拟线程,每个都持有 ThreadLocal
ThreadLocal<byte[]> local = ThreadLocal.withInitial(() -> new byte[1024]);

// ✅ 使用完及时清理
local.remove();

// ✅ 或使用方法参数传递,避免 ThreadLocal

6. 协程在其他语言中的实践

6.1 Kotlin 协程

Kotlin 早在 2018 年就引入了协程,成为 JVM 生态中协程的先行者。Kotlin 的协程是编译期状态机转换,与 Java 21 虚拟线程的 JVM 层面实现有本质区别。

// Kotlin 协程示例
suspend fun fetchData(): String {
    delay(1000)  // 挂起函数,不阻塞线程
    return "data"
}

// 使用协程作用域
runBlocking {
    val result = async { fetchData() }
    println(result.await())
}

6.2 C++20 协程

C++20 也正式引入了协程支持,通过 co_awaitco_yieldco_return 三个关键字实现:

// C++20 协程示例
std::generator<int> fibonacci() {
    int a = 0, b = 1;
    while (true) {
        co_yield a;
        auto next = a + b;
        a = b;
        b = next;
    }
}

6.3 Go goroutine

Go 语言从诞生起就将 goroutine 作为核心特性,其调度模型与 Java 虚拟线程非常相似——都是由运行时管理的 M:N 调度模型。

6.4 各语言协程对比

语言 实现方式 调度 栈大小 成熟度
Java 21 JVM Continuation M:N 动态 ✅ 正式版
Kotlin 编译期状态机 单线程/线程池 无独立栈 ✅ 成熟
Go 运行时 goroutine M:N 2KB 起步 ✅ 成熟
C++20 编译期状态机 用户自定义 无独立栈 ⚠️ 发展中
Python async/await 事件循环 无独立栈 ✅ 成熟
C# async/await + Task 线程池 无独立栈 ✅ 成熟

7. 实际收益:快手透明协程实践

根据快手的技术分享,其基于 Java 17 自研的透明协程方案实现了 30% 以上的吞吐量提升

优化效果

CPU Throttle

FIFO调度无节流

传统线程模型

平均RT: 101ms

协程模型

平均RT: 63ms

QPS上限低

QPS提升30%+

8. 总结:Java 协程的未来

8.1 虚拟线程 vs 第三方协程

方案 优点 缺点
Java 虚拟线程 官方支持、API 兼容、JVM 优化 需要 JDK 21+
Quasar 支持更低 JDK 版本 社区不活跃、需字节码增强
Kotlin 协程 成熟、语法糖丰富 需要学习新语言

8.2 选择建议

JDK 21+

JDK 8/11/17

纯 Java

可引入 Kotlin

选择并发方案

JDK 版本?

使用虚拟线程

语言选择?

传统线程池
或异步框架

Kotlin 协程

I/O 密集型任务
高并发 Web 服务

需评估性能上限

体验现代化并发

8.3 核心要点回顾

要点 总结
协程本质 用户态轻量级任务,可挂起/恢复
与线程区别 协作式 vs 抢占式,极轻量 vs 重资源
Java 方案 Java 21 虚拟线程(Project Loom)
核心优势 高并发 I/O 场景,10 倍+ 吞吐量提升
避坑指南 避免 synchronized 钉住,谨慎使用 ThreadLocal

一句话总结:

协程是一种用户态的轻量级并发模型,通过主动让出机制实现高并发 I/O。Java 21 正式引入的虚拟线程(Virtual Threads)是 Java 官方协程方案,它兼容现有 API,让开发者可以用同步代码写出可扩展至百万级并发的应用,是 Java 并发编程革命性的一步。


📌 本文基于 Java 21 LTS 版本撰写,建议生产环境升级使用。如果你还在使用 JDK 8/11,可以考虑逐步规划升级路线。

如果觉得本文对你有帮助,欢迎点赞、收藏、转发~

在这里插入图片描述


🌺The End🌺点点关注,收藏不迷路🌺
Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐