Scheduling, async and virtual threads

Spring Boot · Advanced · 14 min

Background jobs and concurrency are part of most services, and Java 21 changes how you scale them.

Scheduling: add @EnableScheduling to a configuration class and annotate methods with @Scheduled(fixedRate = 60_000), fixedDelay (wait after completion), or cron = "0 0 3 * * *" (Spring's cron has six fields, starting with seconds). Durations like fixedRateString = "PT1M" or property placeholders keep schedules configurable. In a cluster every instance runs every job; use a lock such as ShedLock, or a dedicated scheduler, for jobs that must run once.

Async methods: @EnableAsync plus @Async on a bean method runs it on a task executor and returns immediately. Return void or CompletableFuture<T>. Like @Transactional, it works through proxies, so calls from the same class run synchronously. Boot auto-configures the executor (spring.task.execution.pool.*).

Virtual threads (Java 21+): set spring.threads.virtual.enabled=true and Boot runs Tomcat request handling, @Async tasks and scheduled tasks on virtual threads. Blocking I/O (JDBC, RestClient calls) then no longer pins a scarce platform thread, so a simple blocking-style service can handle many more concurrent requests without reactive programming.

Limits to know: the connection pool still caps concurrent database work, synchronized blocks around blocking calls can pin carrier threads on JDK 21-23 (fixed in JDK 24), and CPU-bound work gains nothing.

Example

@SpringBootApplication
@EnableScheduling
@EnableAsync
class App { /* main */ }
@Component
class Jobs {
    private static final Logger log = LoggerFactory.getLogger(Jobs.class);
    @Scheduled(fixedDelayString = "${jobs.cleanup.delay:PT5M}")
    void cleanup() { log.info("cleanup on {}", Thread.currentThread()); }
    @Scheduled(cron = "0 0 3 * * *", zone = "Europe/Berlin")   // 03:00 daily
    void nightlyReport() { log.info("report"); }
}
@Service
class ThumbnailService {
    @Async
    public CompletableFuture<String> render(String imageId) {
        // slow blocking work runs on the task executor
        return CompletableFuture.completedFuture("thumb-" + imageId);
    }
}
# application.properties
spring.threads.virtual.enabled=true

Remember

  • @Scheduled with fixedRate, fixedDelay or six-field cron
  • @Async runs bean methods on an executor; return CompletableFuture
  • spring.threads.virtual.enabled=true moves request handling to virtual threads
  • Every instance runs scheduled jobs; lock if they must run once

Watch out for

  • Calling an @Async method from the same class and wondering why it runs synchronously.

Check yourself

  1. What does the Spring cron expression "0 */15 * * * *" mean?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: docs.spring.io/spring-boot/reference/features/task-execution-and-scheduling.html

Builds on

Unlocks

Nothing builds on this yet.