Why can't I start an asynchronous task in a scheduled task using a virtual thread?

454 views Asked by At

I am using Java 21 and Spring Boot 2.7.3. I used java.util.Timer to create a scheduled task, and then called the asynchronous method annotated with @Async annotation in this scheduled task. Then I configured the asynchronous task processor for Spring Boot to be a virtual Thread Executor, finally I printed Thread.currentThread.isVirtual() in the code marked with @Async, and found that false was output. The sample code is as follows (the original intention of the following code is actually to periodically send data to all WebSocket sessions connected to my server):

@Slf4j
@Component
public class TestTask{

    @PostConstruct
    public void pushStatus() {
        Timer timer = new Timer();
        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                countWebSocketsMap.forEach((k, v) -> {
                    pushCount(k,v);
                });
            }
        }, 0, 500);
     }

     @Async
     public void pushCount(String sessionId, WebSocketSession session) {
         log.info("{}",Thread.currentThread().isVirtual());
     }
}

I bound the virtual thread for Spring Boot async tasks like follows:

@EnableAsync
@Configuration
@ConditionalOnProperty(
        value = "spring.thread-executor",
        havingValue = "virtual"
)
@Slf4j
public class AsyncConfig   {
    @Bean
    public AsyncTaskExecutor applicationTaskExecutor() {
        return new TaskExecutorAdapter(Executors.newVirtualThreadPerTaskExecutor());
    }

    @Bean
    public TomcatProtocolHandlerCustomizer<?> protocolHandlerVirtualThreadExecutorCustomizer() {
        return protocolHandler -> {
            protocolHandler.setExecutor(Executors.newVirtualThreadPerTaskExecutor());
        };
    }
}

However, if I call this asynchronous task in a web controller, my console outputs "true", like this:

@GetMapping("/vm")
public void vm( ){
    task();
}

@Async
public void task(){
    log.info("info={}", Thread.currentThread().isVirtual());
}

I am looking for a way to directly use methods annotated with @Async in scheduled tasks, running them in virtual threads. Also, I would like to know if there's a straightforward way to obtain the return value of @Async annotated virtual thread methods.

1

There are 1 answers

4
Mark Rotteveel On

You're calling pushCount from within the class (or more specifically an anonymous inner class of TestTask), and then the Async annotation has no effect. Such annotations usually only work when calling methods from outside on a bean, not when calling from within that bean. That is because these effects are generally achieved by enhancing the object by wrapping it in a proxy that does the actual action of running the method call async, but if you call it within a class, you're calling the method directly on the original object, and not on the wrapper which performs the action in an asynchronous manner.

This BTW also applies to your use in the controller, but there you don't notice it because you have configured the embedded Tomcat to handle requests on virtual threads as well, so you see the result you're looking for, but the method call is not actually async.

The solution is to create a separate bean for the async method, and inject/autowire that into TestTask (or alternatively, to create a separate component for pushStatus() and inject TestTask into that and call the async method on that test task instance). You need to do something similar for your controller if you actually want task() to be really executed async.