Java 21 虚拟线程执行器的性能比具有池化操作系统线程的执行器差?

con*_*rno 3 java executorservice virtual-threads java-21

我刚刚将 Spring Boot 应用程序升级到 Java 21。作为其中的一部分,我还进行了更改以使用虚拟线程。无论是在服务 API 请求时还是在使用执行器在内部执行异步操作时。

对于一种用例,由虚拟线程驱动的执行器的性能似乎比ForkJoinPool由操作系统线程驱动的执行器差。此用例是设置一些 MDC 值并通过 HTTP调用外部系统。

这是我的伪代码:

List<...> ... = executorService.submit(
                () -> IntStream.rangeClosed(-from, to)
                        .mapToObj(i -> ...)
                        .parallel()
                        .map(... -> {
                            try {
                                service.setSomeThreadLocalString(...);
                                MDC.put(..., ...);
                                MDC.put(..., ...);

                                return service.call(...);
                            } finally {
                                service.removeSomeThreadLocalString(...);
                                MDC.remove(...);
                                MDC.remove(...);
                            }
                        })
                        .toList())
        .get();
Run Code Online (Sandbox Code Playgroud)

其中 ExecutorService 是:

  1. new ForkJoinPool(30)
  2. Executors.newVirtualThreadPerTaskExecutor()

看起来选项 1 的性能比选项 2 好很多。有时它比选项 1 快 100%。我在 Java 21 环境中完成了这个测试。我正在测试 10 个并行执行。其中选项 1 通常需要 800-1000 毫秒,选项 2 通常需要 1500-2000 毫秒。

如果有任何区别,请在 Spring Boot 中启用此属性:

spring:
  threads:
    virtual:
      enabled: true
Run Code Online (Sandbox Code Playgroud)

有什么想法为什么会发生这种情况吗?

Hol*_*ger 7

You are assuming that submitting a parallel stream operation as a job to another executor service will make the Stream implementation use that executor service. This is not the case.

There is an undocumented trick to make a parallel stream operation use a different Fork/Join pool by initiating it from a worker thread of that pool. But the executor service producing virtual threads is not a Fork/Join pool.

So when you initiate the parallel stream operation from a virtual thread, the parallel stream will use the common pool for the operation. In other words, you are still using platform threads except for the one initiating virtual thread, as the Stream implementation also performs work in the caller thread.

So when I use the following program

public class ParallelStreamInsideVirtualThread {
    public static void main(String[] args) throws Exception {
        var executorService = Executors.newVirtualThreadPerTaskExecutor();
        var job = executorService.submit(
            () -> {
              Thread init = Thread.currentThread();
              return IntStream.rangeClosed(0, 10).parallel()
                 .peek(x -> printThread(init))
                 .mapToObj(String::valueOf)
                 .toList();
            });
        job.get();
    }
  
    static void printThread(Thread initial) {
        Thread t = Thread.currentThread();
        System.out.println((t.isVirtual()? "Virtual  ": "Platform ")
            + (t == initial? "(initiator)": t.getName()));
    }
}
Run Code Online (Sandbox Code Playgroud)

it will print something like

Virtual  (initiator)
Virtual  (initiator)
Platform ForkJoinPool.commonPool-worker-1
Platform ForkJoinPool.commonPool-worker-3
Platform ForkJoinPool.commonPool-worker-2
Platform ForkJoinPool.commonPool-worker-4
Virtual  (initiator)
Platform ForkJoinPool.commonPool-worker-1
Platform ForkJoinPool.commonPool-worker-3
Platform ForkJoinPool.commonPool-worker-5
Platform ForkJoinPool.commonPool-worker-2
Run Code Online (Sandbox Code Playgroud)

In short, you are not measuring the performance of virtual threads at all.