2

Why is the default scheduler for virtual threads in jdk21 forkjoinpool instead of ThreadPoolExecutor?

The reason I can think of is because in a fork join pool, it is possible to ensure that even if a virtual thread is rebound to a platform thread, it can be bound to the original platform thread, because the fork join pool has an array of workqueues, a task is in a workqueue, and a workqueue corresponds to a platform thread. thread, but this is without considering task stealing, so what else is the reason?

I've noticed that there is no method in jdk21 that enables the user to set the scheduler for virtual threads, as ThreadBuilders are private and the scheduler parameter is not passed in the public ofVirtual method, is this because the scheduler for virtual threads only supports forkjoinpool at the moment?

13
  • Add a stackoverflow.com/help/minimal-reproducible-example to your question. Commented May 30, 2024 at 14:00
  • 7
    The article at blogs.oracle.com/javamagazine/post/… says that the work stealing aspect is the reason why a ForkJoinPool was selected. Commented May 30, 2024 at 14:55
  • No, I don't think that "rebounding to the original platform thread" (did you mean Carrier of a virtual thread?) is what ForkJoinPool is doing (and what would be a benefit of it?). ForkJoinPool is just most advanced concurrent ExecutorService JDK has. In comparison to older 1.5 ThreadPoolExecutor, which uses older AQS-based technique, FJP uses "jdk-internal Unsafe for atomics and special memory modes", which presumably more efficient "in very racy code". Commented Jun 1, 2024 at 14:15
  • 3
    @igor.zh it’s a tenacious myth that Unsafe was faster than alternatives, just because there’s “unsafe” in its name. You may read this article, especially this part of it. That “work-stealing” is an over-hyped feature too. In ordinary thread pool implementations, threads will also pick up any task, if one is available. Since they have no local queue, there isn’t even a need to “steal” a job from another thread to get this done. Commented Jun 4, 2024 at 12:30
  • 1
    @igor.zh no, Shiplev’s article addressed the open issues up to the point that after solving them, the only expensive operation left was the Unsafe operation itself, which happens in either case, as the public API uses the same Unsafe under the hood. Besides that, the term “work-stealing” is questionable in the context of the virtual thread scheduler which does not create F/J Tasks but uses it like any other Executor. Every virtual thread is “stolen”… Commented Jun 7, 2024 at 16:04

1 Answer 1

0

According to a request of the OP to configure VirtualThread with a custom thread pool, the following solution is discussed below.

As of JDK 21 there is no way to set up a custom Executor/scheduler for VirtualThread class: this class along with ThreadBuilders.VirtualThreadBuilder and VirtualThreadFactory are java.lang package-private and the only way to do so is to use Reflection.

public static Thread.Builder customVirtualThreadPool(Executor executor) throws ReflectiveOperationException { var ctor = Class.forName("java.lang.ThreadBuilders$VirtualThreadBuilder") .getDeclaredConstructor(Executor.class); ctor.setAccessible(true); return (Thread.Builder) ctor.newInstance(executor); } 

As access to java.lang classes is attempted it might be necessary to explicitly allow it in application parameters

--add-opens=java.base/java.lang=ALL-UNNAMED 

as Module java.base does not "opens java.lang" thread suggests.

Note that this can be used to set up custom ForkJoinPool instance as an actual pool in order to get an access to work-stealing count.

For Spring Boot/Tomcat application further configuration does not differ from configuration of any other such Executor.

As usual, to run Tomcat worker threads on this Excecutor, the ones that executes @Controllers methods, annotated with @RequestMapping and analogous, customize Tomcat Protocol Handler:

@Bean public TomcatProtocolHandlerCustomizer<?> protocolHandlerVirtualThreadExecutorCustomizer() { return protocolHandler -> { try { protocolHandler.setExecutor( customVirtualThreadPool(Executors.newCachedThreadPool())::start); } catch (ReflectiveOperationException e) { throw new BeanInitializationException("Cannot create custom Virtual Thread Executor", e); } }; } 

Similarly, to run Spring-controlled asynchronous methods on this Executor, configuration will look like this:

@Configuration @EnableAsync public class AppConfig { @Bean public TaskExecutor taskExecutor() { try { return new TaskExecutorAdapter(customVirtualThreadPool(Executors.newCachedThreadPool())::start); } catch (ReflectiveOperationException e) { throw new BeanInitializationException("Cannot create custom Virtual Thread Executor", e); } } } 

and the asynchronous method like this:

@Service @Async // will use `taskExecutor` by default, if there are more than one in the app, then Bean name can be specified public class AsyncService { public void doSomething() { ... } } 

Note that presence of spring.threads.virtual.enabled = true in application property file is not required in both cases.

The above is applicable to Spring Boot 3.2.0; be cautious with older Tomcats, they may or may not support the configuration above.

Sign up to request clarification or add additional context in comments.

8 Comments

The crucial part is the reflective construction of the thread builder. There is no need to keep using Reflection after that. There’s also no point in catching all those subclasses of ReflectiveOperationException, to wrap them in a new ReflectiveOperationException. So you can simplify it to static Thread.Builder customVirtualThreadPool(Executor executor) throws ReflectiveOperationException { var ctor = Class.forName("java.lang.ThreadBuilders$VirtualThreadBuilder").getDeclaredConstructor(Executor.class); ctor.setAccessible(true); return (Thread.Builder)ctor.newInstance(executor); }
This static method can be used together with the public API, e.g. Executor e = customVirtualThreadPool(pool)::start; or Executors.newThreadPerTaskExecutor(customVirtualThreadPool(pool).factory()) or even applying more customization: Executors.newThreadPerTaskExecutor(customVirtualThreadPool(pool) .inheritInheritableThreadLocals(false).name("my-custom-thread-", 100).factory())
the takeaway is, since his suggested improvements were made, there is no reason left for developers to use Unsafe directly. The higher level APIs are at least on par. But it’s even possible that a future improvement will only accelerate the official API as Unsafe itself is the remaining bottleneck and the faster solution can only work without. Well, there are also practical advice you can read from his article, like storing a VarHandle in a static final field is a good idea to get maximum performance with today’s implementations.
Well, the optimized code of today is the legacy of tomorrow. The built-in classes have the advantage that they can get adapted with every Java version. If found this comment which suggests that the reason ForkJoinPool uses Unsafe in recent versions is not performance: “[…] should be using VarHandles. It was attempted for ForkJoinPool in JDK 19 but had to be reverted back to Unsafe due to bootstrapping and initialization issues…
The term “work-stealing” puts an emphasis on the wrong thing, as stealing is how all thread pools work. What sets F/J apart, is the existence of local queues in the first place, including the ability of a task to revoke a submission and execute the subtask locally, if it has not stolen yet. So you can create subtasks at a much higher granularity, to ensure saturation of worker threads even when tasks take significantly different time, without the overhead of a too large number of asynchronous tasks. A high steal count is not necessarily a good sign (but also not necessarily a bad sign).
|

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.