当前位置:首页 > 文章列表 > 文章 > java教程 > Java虚拟线程承载阻塞I/O服务的线程模型设计

Java虚拟线程承载阻塞I/O服务的线程模型设计

来源:17golang原创 2026-09-20 08:31:38 0浏览 收藏

Java 虚拟线程适合承载“请求很多、每个请求大部分时间都在等待”的服务。把一个请求放进 Executors.newVirtualThreadPerTaskExecutor() 后,数据库查询、HTTP 调用或 socket 读取发生阻塞时,运行时可以挂起虚拟线程并释放承载它的操作系统线程;但 CPU 密集计算不会因此变快,连接池、下游限流和内存也不会自动扩大。

要点速览
  • 使用 JDK 21 及以上,把虚拟线程按任务创建,不要把它们再放进固定大小线程池。
  • 虚拟线程数量与数据库连接数、HTTP 并发额度是两件事,有限资源要用信号量或资源池单独限流。
  • 排序、压缩、加密等 CPU 密集工作应交给有界的平台线程执行器;迁移后重点观察 pinning、队列长度和下游耗时。
实际落地时可以把请求处理保留为同步写法,把“每个请求一个线程”换成“每个任务一个虚拟线程”;只在等待型 I/O 周围放宽并发,在 CPU 计算和外部有限资源处设置独立边界。

先判断请求是在等待,还是在计算

虚拟线程的价值是提高并发承载量,不是让某一段 Java 代码跑得更快。一个请求如果大部分时间在等待数据库、HTTP、文件或队列,传统平台线程会长期占住操作系统线程;虚拟线程则可以在 JDK 支持的阻塞操作处暂停,把承载线程交给其他任务。相反,视频转码、复杂排序、密码计算等 CPU 密集工作会持续占用处理器,创建更多线程只会增加调度和上下文压力。

Java虚拟线程、平台线程承载和阻塞I/O释放关系的静态结构图
图1:虚拟线程在计算区运行于承载平台线程,进入阻塞 I/O 后可以暂停并释放承载线程;图中表达资源关系,不表示固定执行顺序。

用按任务创建的虚拟线程承载阻塞 I/O

JDK 21 已将虚拟线程作为正式能力提供。下面的最小服务骨架把每个请求交给一个虚拟线程,并通过 try 作用域在关闭执行器时等待已提交任务结束。代码中的 callRemoteService 可以替换成 JDBC、HTTP Client 或文件 API;示例重点是线程模型,而不是某个框架的路由写法。

import java.time.Duration;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executors;

static String handleRequest(String request) throws Exception {
    // 每个任务创建一个虚拟线程,避免把虚拟线程当作需要复用的昂贵资源。
    try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
        var remote = executor.submit(() -> callRemoteService(request));
        // get() 只会让当前虚拟线程等待,不应把等待误判为 CPU 计算。
        return remote.get();
    } catch (InterruptedException e) {
        // 保留中断标记,让上层能够停止取消后的请求处理。
        Thread.currentThread().interrupt();
        throw e;
    } catch (ExecutionException e) {
        // 对外抛出业务异常时保留原始 cause,便于记录真正的 I/O 错误。
        throw new IllegalStateException("下游调用失败", e.getCause());
    }
}

static String callRemoteService(String request) throws InterruptedException {
    // 这里代表一个可能阻塞的网络或数据库调用;生产代码应设置超时。
    Thread.sleep(Duration.ofMillis(20));
    return "processed:" + request;
}

不要在每个请求里创建一个长期存活的共享执行器。更常见的做法是把执行器作为应用组件创建并复用,任务提交仍然是“一任务一虚拟线程”。如果业务希望并发地调用多个下游服务,可在同一个作用域提交多个任务,再集中等待和处理异常。

给数据库和下游接口加独立并发闸门

虚拟线程便宜,不代表数据库连接、第三方配额或文件句柄无限。把固定线程池改成虚拟线程后,最容易出现的回归是请求进来了很多,但所有请求同时争抢一个只有几十个连接的资源池。此时不应重新把虚拟线程池改小,而应在资源边界使用 Semaphore 或让连接池本身承担排队。

import java.util.concurrent.Semaphore;

final class DownstreamGate {
    // permits 表示允许同时占用的下游名额,而不是虚拟线程总数。
    private final Semaphore permits = new Semaphore(32);

    String query(String key) throws Exception {
        permits.acquire();
        try {
            // 只有真正占用下游连接的区间才持有许可,减少无谓排队。
            return blockingQuery(key);
        } finally {
            // 无论查询成功、失败还是取消,都必须归还名额。
            permits.release();
        }
    }
}
Java虚拟线程请求层、Semaphore闸门、连接池和CPU执行器之间边界的静态关系图
图2:请求可以由大量虚拟线程承载,但数据库连接、下游配额和 CPU 执行器各有自己的边界,需要分别配置。

CPU 密集阶段使用有界平台线程执行器

如果请求先读取数据,再做大规模排序或压缩,可以让虚拟线程负责前后的 I/O,把计算段提交到大小接近处理器数量的有界执行器。这样做的目的不是追求一个固定数字,而是避免无界请求把大量 CPU 任务同时排入内存;具体大小应根据压测、任务时长和业务延迟目标调整。

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

final class Workloads implements AutoCloseable {
    private final ExecutorService cpuExecutor =
        Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors());

    String process(String key) throws Exception {
        // 外层可运行在虚拟线程,阻塞读取完成后才进入 CPU 阶段。
        var input = blockingRead(key);
        var future = cpuExecutor.submit(() -> expensiveTransform(input));
        // 等待计算结果时保留中断语义,不吞掉取消信号。
        return future.get();
    }

    @Override
    public void close() {
        // 应用退出时关闭平台线程执行器,避免遗留非守护线程。
        cpuExecutor.shutdown();
    }
}

迁移后检查锁、资源和可观测性边界

迁移并不是把线程工厂名替换掉就结束。长时间阻塞的 synchronized 区域可能使虚拟线程 pin 住承载平台线程;应缩小锁保护范围,并用 JFR 的虚拟线程事件观察实际影响。线程本地变量也要重新评估:虚拟线程不复用,不能再依靠“同一个线程反复拿到同一个昂贵连接”来做资源缓存。

工作负载建议模型边界检查
大量 HTTP、JDBC 或文件等待按任务创建虚拟线程超时、取消、连接池和下游配额
排序、压缩、加密等计算有界平台线程执行器处理器利用率、队列和尾延迟
同一共享状态的复合更新缩小锁范围或改为消息串行化避免在锁内执行阻塞 I/O
迁移后的异常排查JFR、日志关联和压测pinning、内存、取消和资源泄漏

常见问题

虚拟线程是否应该放进固定大小线程池?

通常不应该。虚拟线程应按任务创建;如果要限制数据库或第三方服务并发,应限制那个具体资源,而不是用线程池伪装资源闸门。

阻塞 I/O 一定会释放平台线程吗?

JDK 支持的阻塞 I/O 通常可以挂起虚拟线程,但长时间持有某些锁、调用不配合的本地代码或外部库时,仍要通过 JFR 和压测确认是否出现 pinning。

虚拟线程会降低单次 CPU 任务耗时吗?

不会。它主要改善高并发等待型任务的吞吐;CPU 密集任务仍受处理器核心数限制,应该使用有界的计算执行器。

落地时可以先选择一条以 I/O 等待为主的接口,记录连接池利用率、P95/P99 延迟、CPU 和取消率,再用虚拟线程替换原来的请求执行模型。只要把请求并发、外部资源并发和 CPU 并发拆成三个可以独立观测的数字,线程模型就不容易在扩容时失控。

版本声明
本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
Go JSON字符串自动转义HTML字符的关闭边界Go JSON字符串自动转义HTML字符的关闭边界
上一篇
Go JSON字符串自动转义HTML字符的关闭边界
LibTV第一次做AI成片:先完成30秒样片的入门流程
下一篇
LibTV第一次做AI成片:先完成30秒样片的入门流程
查看更多
最新文章
查看更多
课程推荐
  • 前端进阶之JavaScript设计模式
    前端进阶之JavaScript设计模式
    设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
    543次学习
  • GO语言核心编程课程
    GO语言核心编程课程
    本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
    516次学习
  • 简单聊聊mysql8与网络通信
    简单聊聊mysql8与网络通信
    如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
    500次学习
  • JavaScript正则表达式基础与实战
    JavaScript正则表达式基础与实战
    在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
    487次学习
  • 从零制作响应式网站—Grid布局
    从零制作响应式网站—Grid布局
    本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
    485次学习
查看更多
AI推荐
  • PubMedQA数据集详解:生物医学问答基准、功能与应用指南
    PubMedQA
    深入了解PubMedQA生物医学问答数据集,涵盖其核心功能、使用方法及在临床决策、药物研发等场景的应用,助力提升NLP模型性能。
    129次使用
  • H2O EvalGPT:开源LLM大模型评估与排行榜工具
    H2O EvalGPT
    H2O EvalGPT是H2O.ai推出的开源LLM评估平台,提供详细的大模型性能排行榜、行业特定基准测试及A/B测试功能,助您快速选择最适合项目的高性能大语言模型。
    198次使用
  • LMArena是什么?伯克利AI模型评估平台使用指南与功能解析
    LMArena
    LMArena是加州大学伯克利分校推出的AI模型匿名评测平台。通过盲测投票机制,用户可对比不同大模型回答并生成实时排行榜,助力开发者优化模型及用户选择最佳AI工具。
    143次使用
  • 斯坦福HELM:大语言模型Holistic Evaluation整体评估框架详解
    HELM
    深入了解斯坦福推出的HELM(Holistic Evaluation of Language Models)大模型评测体系。本文解析其核心功能、安装配置步骤及应用场景,涵盖准确性、公平性、鲁棒性等多维度指标,助力开发者全面优化语言模型性能。
    118次使用
  • CMMLU中文大模型评估基准:功能、使用教程与应用场景解析
    CMMLU
    深入了解CMMLU中文评估基准,涵盖67个学科主题,提供数据集下载、Zero-shot/Five-shot评估方法及排行榜,助力优化中文语言模型性能。
    106次使用
微信登录更方便
  • 密码登录
  • 注册账号
登录即同意 用户协议隐私政策
返回登录
  • 重置密码