Skip to content

开启虚拟线程(Virtual Thread) ​

一、背景说明 ​

自 Java 21 起,虚拟线程(Virtual Thread) 正式成为稳定特性(JEP 444),为高并发场景提供了一种低成本、高并发、阻塞友好的线程模型。

虚拟线程由 JVM 调度,不再与操作系统线程一一对应,在面对大量阻塞型任务(如数据库、RPC、文件 IO)时,能够显著降低线程创建、上下文切换以及内存占用成本。

t-io / tio-boot 作为高性能网络通信框架,在 Java 21 环境下,可以通过自定义 ThreadFactory 或 ExecutorService 的方式,让业务处理逻辑运行在虚拟线程之上,从而显著提升并发能力和资源利用率。


二、tio-core 中的线程模型说明 ​

在 tio-core 中,线程大致可以分为以下三类(非常重要):

1. smart-common 线程(事件循环线程) ​

  • 固定 2 个平台线程

  • 启动并长期运行:

    • commonWorker
    • writeWorker

主要职责:

  • 处理 Accept / Connect / Write 事件

  • 触发:

    • doAccept()
    • doWrite()
    • connect runnable.run()
    • 少量“同步 read”分支中的 doRead()

说明:

  • 这是一组 Selector / Reactor 事件循环线程
  • 长期常驻运行(while(select))
  • 对延迟和稳定性要求极高
  • 不适合承载业务逻辑

2. readWorkers(Read 事件循环线程) ​

  • 数量可配置

  • 默认用于:

    • 处理 Read 就绪事件
    • 数据读取
    • 协议解码(ByteBuffer → Packet)
    • 在未设置 bizExecutor 时,直接执行 handler

特点:

  • 仍然是 事件循环线程
  • 不是 per-task 模型
  • 数量通常与 CPU 核数相关
  • 可通过 setWorkThreadFactory + setWorkThreadNum 配置

3. 业务执行线程(BizExecutor) ​

  • 默认未启用

  • 当未设置 bizExecutor 时:

    • handler 由 readWorkers 线程直接执行
  • 当设置 bizExecutor 后:

    • handler 会被投递到业务线程池执行
    • readWorkers 只负责 decode + 分发

说明:

  • 这是 最适合使用虚拟线程的一层

  • 可以是:

    • 平台线程池(Java 8)
    • 虚拟线程 Executor(Java 21)

补充说明(重要实测结论) ​

实测发现: 在返回纯文本、业务逻辑极轻且无阻塞的场景下,不设置 bizExecutor,直接由 readWorkers 执行 handler,性能确实非常高。

但需要注意:

  • 该结论 仅适用于:

    • handler 不阻塞
    • handler 极短
    • 不访问数据库 / RPC / 文件 IO
  • 一旦 handler 出现阻塞或长尾,请务必启用 bizExecutor


三、Java 版本要求 ​

必须使用 Java 21 或以上版本

原因:

  • Java 19 / 20 中虚拟线程仍为预览特性
  • Java 21 起虚拟线程正式 GA
  • Thread.ofVirtual()、Executors.newThreadPerTaskExecutor(...) 在 Java 21 中稳定可用

可通过以下命令确认版本:

bash
java -version

四、示例代码(tio-boot 中启用虚拟线程) ​

下面是一个在 tio-boot 中启用虚拟线程作为业务执行线程的最小示例。

java
package example;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

import nexus.io.tio.boot.TioApplication;
import nexus.io.tio.boot.server.TioBootServer;

public class Application {
  public static void main(String[] args) {
    ExecutorService businessExecutor = Executors.newThreadPerTaskExecutor(
        Thread.ofVirtual().name("t-biz-v-", 0).factory());

    // Configure the executor before starting the server.
    TioBootServer server = TioBootServer.me();
    server.setBizExecutor(businessExecutor);
    Runtime.getRuntime().addShutdownHook(
        new Thread(businessExecutor::shutdownNow, "business-executor-shutdown"));

    TioApplication.run(Application.class, args);
  }
}

配置时机与执行范围 ​

应在 TioApplication.run(...) 之前调用 setBizExecutor(...)。框架初始化网络服务时,会将业务执行器传入运行时配置;普通 BootConfiguration.config() 用于后续应用配置,不适合放置这项启动设置。已有应用使用带 BootConfiguration 参数的 run 重载时,也应先在入口中设置业务执行器,再调用该重载。

上述示例仅配置业务执行器,保留网络 I/O 工作线程的默认配置。这样,等待上游 HTTP 响应等阻塞业务可以在虚拟线程中执行,网络工作线程继续接收和解码请求。同一连接内的业务任务仍按框架的顺序执行,不同连接可以并发处理。

可以在业务 Handler 中使用 Thread.currentThread().isVirtual() 验证执行线程。并发验证时,应使用多个独立连接,让部分请求等待本地模拟服务,同时检查其他接口是否仍能及时响应。

JDK 21 中,JNI 调用以及部分同步代码可能固定虚拟线程的载体线程。数据库查询仍应配合索引、合理的查询范围和并发控制;虚拟线程可以降低等待的平台线程成本,但不会自动减少 SQL 执行时间。


五、重要说明(非常关键) ​

1. tio-boot 并非所有线程都是虚拟线程 ​

需要特别注意:

tio-boot 在处理“IO 就绪事件(SelectionKey)”时,使用的仍然是平台线程,而不是虚拟线程。

具体如下:

IO / Reactor 事件循环线程 ​

  • Selector / Reactor 线程
  • smart-common
  • readWorkers
  • 全部为平台线程
  • 数量有限
  • 与 CPU 核数强相关

业务处理线程 ​

  • 通过 setBizExecutor(...) 注入
  • 可以使用 虚拟线程
  • 数量不再受 OS 线程数限制

这是一个标准的 Reactor + Worker 架构,也是当前虚拟线程在网络框架中的最佳实践模型。


2. 为什么不把 IO 线程改成虚拟线程? ​

原因包括:

  • Selector / Reactor 是:

    • 常驻事件循环
    • 非 per-task 模型
  • 虚拟线程最适合:

    • 阻塞式业务逻辑
    • JDBC / Redis / RPC
    • 文件 IO
  • Reactor 线程数量本身很少,通常不是性能瓶颈

因此,将 虚拟线程用于业务层,而不是 IO 事件循环层,是当前最合理、最稳定的设计。


六、适用场景分析 ​

启用虚拟线程后,tio-boot 非常适合以下场景:

  • 高并发长连接

    • WebSocket
    • TCP
  • 阻塞型业务逻辑

    • JDBC
    • Redis
    • HTTP / RPC 调用
  • Benchmark / 压测场景

  • 单请求逻辑不复杂,但并发量极高的服务端应用


七、总结 ​

  • Java 21 起,虚拟线程可安全用于生产环境
  • tio-boot 支持通过 ThreadFactory / ExecutorService 注入虚拟线程
  • 业务处理线程非常适合使用虚拟线程
  • IO / Reactor 事件循环线程仍应使用平台线程
  • 这种混合模型在性能、稳定性与可扩展性之间取得了良好平衡

MIT Licensed