使用Java虚拟线程比Node.js性能更高


Java 19 中引入的虚拟线程旨在加速并发网络请求。在这篇文章中,我想比较发出 HTTP 请求的常规线程和虚拟线程的吞吐量。为此,我在谷歌云中使用了两个虚拟机。每台机器有 8 个 CPU 和 16 GB 内存。一台机器将作为服务器,另一台作为客户端。

服务器计算机运行一个小型 Spring Boot 应用程序。

@SpringBootApplication 
@RestController 
public  class  Main { 

    public  static  void  main (String[] args) { 
        SpringApplication.run(Main.class, args); 
    } 

    @GetMapping("/") 
    public String hello ( @RequestParam(value = "i") String i) { 
        return i; 
    } 
}

在客户端应用程序中,我使用返回值的总和作为一个简单的控件来确保所有并发请求都产生有效响应。

客户端应用程序使用传统的缓存线程池和单独的虚拟线程发送相同的并发 HTTP 请求集。每个HTTP请求的序号赋值给参数i:

public class Network {

    List<Callable<String>> tasks;
    int repeats;

    public Network(String[] args) {

        var ip = args[2];
        var urls = IntStream.range(0, Integer.parseInt(args[0]))
                .mapToObj(i -> "http://" + ip + ":8080/?i=" + i).toList();
        tasks = urls.stream().map(url -> (Callable<String>) () -> fetchURL(url)).toList();

        repeats = Integer.parseInt(args[1]);
    }

    HttpClient client = HttpClient.newBuilder().followRedirects(HttpClient.Redirect.NEVER).build();

    String fetchURL(String url) throws IOException, InterruptedException {
        var request = HttpRequest.newBuilder().uri(URI.create(url)).build();
        return client.send(request, HttpResponse.BodyHandlers.ofString()).body();
    }

    String execute(ExecutorService executor) throws Exception {
        try (executor) {
            var s = System.currentTimeMillis();
            var sum = executor.invokeAll(tasks).stream().mapToInt(f -> Integer.valueOf(f.resultNow())).sum();
            return (System.currentTimeMillis() - s) + "\t" + sum;
        }
    }

    void assessExecutors() throws Exception {
        out.println("CPU count " + Runtime.getRuntime().availableProcessors());
        out.println("cached\t\t\tvirtual");
        out.println("time\tsum\t\ttime\tsum");
        for (var i = 0; i < repeats; i++) {
            var cached = execute(Executors.newCachedThreadPool());
            var virtual = execute(Executors.newVirtualThreadPerTaskExecutor());
            out.println(cached + "\t\t" + virtual);
        }
    }

    public static void main(String[] args) throws Exception {
        new Network(args).assessExecutors();
    }
}


主网络类希望得到三个命令行参数:要发送的并发请求的数量,重复测量的次数,以及服务器机器的IP地址。

与官方文档中使用的睡眠任务不同,即使有1000个并发请求,普通线程和虚拟线程之间的差异也是可见的。

在虚拟线程中,网络请求的执行速度持续提高了~40%

在3000或5000个并发请求的情况下,可以看到同样的性能改进

如果有10000个并发请求,服务器就会停止响应,直到重新启动。我想这是虚拟机的限制,而不是Java的限制。

虚拟线程使用的机制让我想起了 Node.js。在 Node.js 中,JavaScript 代码由一个线程执行,但网络请求是异步的,并由所有可用的操作系统线程执行。因此,Java 在网络请求吞吐量方面无法明显优于 Node.js。

因此,由于我已准备好实验设置,因此我再次将 Java 的性能与 Node.js 进行了比较。现在,对同一服务器的相同并发请求由 JavaScript 代码发送:

import { argv } from 'process';
const [requests, repeats, ip] = argv.slice(2);

const urls = Array.from({ length: requests }, (e, i) => `http://${ip}:8080/?i=${i}`);
console.log("attempt\ttime\tsum");
for (let i = 0; i < repeats; i++) {
    const start = Date.now();
    const contents = await Promise.all(urls.map(url =>
        fetch(url).then(res => res.text())));
    console.log(i+"\t"+(Date.now() - start)+"\t"+contents.map(s => parseInt(s)).reduce((t, v) => t + v, 0));
}


我用Node.js 19执行了该代码。该代码在5000个请求时持续失败.

上面的Java客户端在处理5000个请求时没有出现异常,所以问题似乎出在Node.js或其脆弱的fetch()方法上。'

所以,Java HTTP客户端似乎更有能力。

在虚拟线程方面,Java显然胜过Node.js:

NodeJS 3000并发平均用时:400ms
Java虚拟线程3000并发平均用时:220ms
Java虚拟线程5000并发平均用时:330ms

虚拟线程是Java的一项重要改进。从事高吞吐量应用的开发者可能会从这个新功能中受益。