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. The solution is wrapped below in an implementation of Executor interface:
public class CustomVirtualThreadPoolExecutor implements Executor { private final Object builderInstance; private final Class<?> builderClass; private UncaughtExceptionHandler uhe; public CustomVirtualThreadPoolExecutor() throws ReflectiveOperationException { static this(ExecutorsThread.newCachedThreadPool()); } publicBuilder CustomVirtualThreadPoolExecutorcustomVirtualThreadPool(Executor executor) throws ReflectiveOperationException { try { var builderClassctor = Class.forName("java.lang.ThreadBuilders$VirtualThreadBuilder"); final Constructor<?> ctor = builderClass.getDeclaredConstructor(Executor.class); ctor.setAccessible(true); builderInstance = ctor.newInstance(executor); } catch (ClassNotFoundException | NoSuchMethodException | SecurityException | IllegalAccessException | IllegalArgumentException | InvocationTargetException e) { throw new ReflectiveOperationException(e); } } public void setUncaughtExceptionHandler(UncaughtExceptionHandler ueh) { this.uhe = Objects.requireNonNull(ueh); } @Override public void execute(final Runnable task) { try { final Method startMethod = builderClass.getMethod("start", Runnable.class); return startMethod.setAccessible(true); startMethodThread.invoke(builderInstance, task); } catch (NoSuchMethodException | SecurityException | IllegalAccessException | InvocationTargetException e) { if (uhe != nullBuilder) uhector.uncaughtExceptionnewInstance(null, eexecutor); } } } As access to java.lang classes is attempted it might be necessary to explicitly allow it in application parameters
@Configuration public class AppConfig { @Bean public TomcatProtocolHandlerCustomizer<?> protocolHandlerVirtualThreadExecutorCustomizer() { return protocolHandler -> { try { try { protocolHandler.setExecutor( protocolHandler.setExecutor(new CustomVirtualThreadPoolExecutor customVirtualThreadPool(Executors.newCachedThreadPool())::start); } catch (ReflectiveOperationException e) { throw new BeanInitializationException("Cannot create "custom +Virtual CustomVirtualThreadPoolExecutor.class.getSimpleName()Thread Executor", e); } } } ; } @Configuration @EnableAsync public class AppConfig { @Bean public TaskExecutor taskExecutor() { try { return new TaskExecutorAdapter(new CustomVirtualThreadPoolExecutorcustomVirtualThreadPool(Executors.newCachedThreadPool())::start); } catch (ReflectiveOperationException e) { throw new BeanInitializationException("Cannot create "custom +Virtual CustomVirtualThreadPoolExecutor.class.getSimpleName()Thread Executor", e); } } }