尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

10000 个并发任务,虚拟线程 1 秒 vs 传统线程池 50 秒,差距在哪?

10000 个并发任务,虚拟线程 1 秒 vs 传统线程池 50 秒,差距在哪? 大家好我是Java1234_小锋老师。同样是让 10000 个任务各等待 1 秒固定大小为 200 的传统线程池大约要 50 秒虚拟线程却可能只用 12 秒。它并不是把 CPU 变快了而是让“等待”不再白白占着宝贵的平台线程。先说结论这个标题里的“1 秒”和“50 秒”有一个很重要的前提任务大部分时间都在等待例如等待数据库、远程接口、文件或者消息返回。如果 10000 个任务都要等待 1 秒传统线程池只有 200 个线程那么它只能分 50 批处理10000 ÷ 200 × 1 秒 50 秒虚拟线程则可以近似做到“一个任务一个线程”。当某个任务开始等待时它会暂时让出底层平台线程让平台线程继续执行别的任务。因此10000 个等待可以大量重叠总耗时会接近单次等待时间而不是把每一批的时间累加起来。不过这不是一条通用性能定律。真实耗时会受到机器配置、JDK 版本、数据库连接池和下游接口承载能力等因素影响。为什么会出现 1 秒和 50 秒的差距传统 Java 线程通常会对应到操作系统线程。操作系统线程并不轻创建它需要内存切换它也有成本所以项目里一般会使用固定大小的线程池。线程池能控制资源却也形成了“窗口数量”的限制。200 个窗口同时只能接待 200 个任务剩下的任务只能排队。虚拟线程由 JVM 管理比平台线程轻得多。我们可以创建成千上万个虚拟线程再由少量平台线程负责真正执行。它更像是给每位顾客发一个号码牌顾客等待资料时窗口可以先服务其他人不必陪着一起等。固定线程池 200虚拟线程10000 个任务到达使用哪种线程200 个执行其余排队约 50 批完成每个任务拥有虚拟线程I/O 等待时让出平台线程大量等待时间相互重叠用一段代码实际对比下面的示例需要JDK 21 或更高版本。Thread.sleep(1000)用来模拟一次耗时 1 秒的网络或数据库请求。importjava.time.Duration;importjava.time.Instant;importjava.util.concurrent.ExecutorService;importjava.util.concurrent.Executors;importjava.util.concurrent.TimeUnit;/** * 传统线程池与虚拟线程吞吐量对比示例。 */publicclassVirtualThreadBenchmark{privatestaticfinalintTASK_COUNT10_000;/** * 程序入口分别执行两种线程模型。 */publicstaticvoidmain(String[]args)throwsInterruptedException{run(传统固定线程池,Executors.newFixedThreadPool(200));run(虚拟线程,Executors.newVirtualThreadPerTaskExecutor());}/** * 提交任务并统计全部任务完成所需的时间。 * * param name 测试名称 * param executor 待测试的执行器 */privatestaticvoidrun(Stringname,ExecutorServiceexecutor)throwsInterruptedException{InstantstartInstant.now();try(executor){for(inti0;iTASK_COUNT;i){executor.submit(()-{try{// 模拟等待远程接口、数据库或文件返回Thread.sleep(1_000);}catch(InterruptedExceptionexception){// 恢复中断标记交给上层决定如何处理Thread.currentThread().interrupt();}});}executor.shutdown();executor.awaitTermination(2,TimeUnit.MINUTES);}longelapsedDuration.between(start,Instant.now()).toMillis();System.out.printf(%s%.2f 秒%n,name,elapsed/1_000.0);}}一次典型的测试结果可能是传统固定线程池50.18 秒 虚拟线程1.12 秒这里快的不是sleep本身而是 10000 次等待几乎同时发生了。若把任务换成复杂计算例如压缩视频或计算哈希虚拟线程通常不会带来这种数量级的提升因为 CPU 依然只有那么多核心。虚拟线程到底做了什么可以把平台线程看作真正干活的“工人”虚拟线程则是轻量的“任务单”。当虚拟线程执行普通 Java 代码时JVM 会把它挂载到某个平台线程上。遇到支持虚拟线程的阻塞操作后虚拟线程会被卸载平台线程转身处理其他任务。等数据返回原来的虚拟线程再找合适的平台线程继续运行。整个过程对业务代码很友好我们仍然可以按照从上到下的同步写法编程不需要为了提高并发量把代码拆成许多回调。适合放到哪些业务里虚拟线程最适合“请求很多、等待很多、计算不重”的场景例如调用多个远程接口订单详情需要同时查询商品、库存、优惠和物流。数据库访问大量请求都在等待 SQL 返回但要注意连接池仍然有容量上限。批量文件处理读取大量小文件或者上传、下载对象存储中的文件。消息消费和网关服务每个任务逻辑不复杂却经常等待网络响应。例如同时查询三个远程服务时可以直接为每个查询创建一个虚拟线程importjava.util.concurrent.Executors;importjava.util.concurrent.Future;/** * 使用虚拟线程并行查询订单相关信息。 */publicclassOrderDetailService{/** * 并行获取订单、库存和物流信息。 * * return 汇总后的订单详情 */publicStringloadOrderDetail()throwsException{try(varexecutorExecutors.newVirtualThreadPerTaskExecutor()){FutureStringorderexecutor.submit(()-queryOrder());FutureStringstockexecutor.submit(()-queryStock());FutureStringdeliveryexecutor.submit(()-queryDelivery());returnorder.get()stock.get()delivery.get();}}/** 查询订单信息。 */privateStringqueryOrder()throwsInterruptedException{Thread.sleep(300);return订单信息;}/** 查询库存信息。 */privateStringqueryStock()throwsInterruptedException{Thread.sleep(400);return库存信息;}/** 查询物流信息。 */privateStringqueryDelivery()throwsInterruptedException{Thread.sleep(500);return物流信息;}}三个查询顺序执行大约需要 1.2 秒并行后则接近最慢的那一次也就是 0.5 秒。虚拟线程也不是万能药使用前还要留意几件事它不能突破下游限制。数据库连接池只有 50 个连接创建 10000 个虚拟线程也不会变出更多连接。CPU 密集任务不会凭空变快。计算量没减少过多并发反而可能增加调度开销。不要把虚拟线程池化。虚拟线程本身很轻通常直接按任务创建需要限制并发时应使用信号量等方式保护下游资源。压测要贴近真实业务。sleep只是帮助理解原理生产环境还要观察超时、内存、连接数和错误率。最后总结“虚拟线程 1 秒传统线程池 50 秒”的核心不是虚拟线程拥有更强的计算能力而是它大幅降低了线程的使用成本让大量 I/O 等待可以同时发生。对于高并发、I/O 密集型 Java 服务虚拟线程让我们既能保留清晰的同步代码又能获得接近异步编程的吞吐能力。升级到 JDK 21 后它很值得放进真实接口和压测环境里试一试——但别只盯着线程数量也要一起检查数据库、连接池和下游服务能不能接得住。
返回列表