eBook – Guide Spring Cloud – NPI EA (cat=Spring Cloud)
announcement - icon

Let's get started with a Microservice Architecture with Spring Cloud:

>> Join Pro and download the eBook

eBook – Mockito – NPI EA (tag = Mockito)
announcement - icon

Mocking is an essential part of unit testing, and the Mockito library makes it easy to write clean and intuitive unit tests for your Java code.

Get started with mocking and improve your application tests using our Mockito guide:

Download the eBook

eBook – Java Concurrency – NPI EA (cat=Java Concurrency)
announcement - icon

Handling concurrency in an application can be a tricky process with many potential pitfalls. A solid grasp of the fundamentals will go a long way to help minimize these issues.

Get started with understanding multi-threaded applications with our Java Concurrency guide:

>> Download the eBook

eBook – Reactive – NPI EA (cat=Reactive)
announcement - icon

Spring 5 added support for reactive programming with the Spring WebFlux module, which has been improved upon ever since. Get started with the Reactor project basics and reactive programming in Spring Boot:

>> Join Pro and download the eBook

eBook – Java Streams – NPI EA (cat=Java Streams)
announcement - icon

Since its introduction in Java 8, the Stream API has become a staple of Java development. The basic operations like iterating, filtering, mapping sequences of elements are deceptively simple to use.

But these can also be overused and fall into some common pitfalls.

To get a better understanding on how Streams work and how to combine them with other language features, check out our guide to Java Streams:

>> Join Pro and download the eBook

eBook – Jackson – NPI EA (cat=Jackson)
announcement - icon

Do JSON right with Jackson

Download the E-book

eBook – HTTP Client – NPI EA (cat=Http Client-Side)
announcement - icon

Get the most out of the Apache HTTP Client

Download the E-book

eBook – Maven – NPI EA (cat = Maven)
announcement - icon

Get Started with Apache Maven:

Download the E-book

eBook – Persistence – NPI EA (cat=Persistence)
announcement - icon

Working on getting your persistence layer right with Spring?

Explore the eBook

eBook – RwS – NPI EA (cat=Spring MVC)
announcement - icon

Building a REST API with Spring?

Download the E-book

Course – LS – NPI EA (cat=Jackson)
announcement - icon

Get started with Spring and Spring Boot, through the Learn Spring course:

>> LEARN SPRING
Course – RWSB – NPI EA (cat=REST)
announcement - icon

Explore Spring Boot 3 and Spring 6 in-depth through building a full REST API with the framework:

>> The New “REST With Spring Boot”

Course – LSS – NPI EA (cat=Spring Security)
announcement - icon

Yes, Spring Security can be complex, from the more advanced functionality within the Core to the deep OAuth support in the framework.

I built the security material as two full courses - Core and OAuth, to get practical with these more complex scenarios. We explore when and how to use each feature and code through it on the backing project.

You can explore the course here:

>> Learn Spring Security

Course – LSD – NPI EA (tag=Spring Data JPA)
announcement - icon

Spring Data JPA is a great way to handle the complexity of JPA with the powerful simplicity of Spring Boot.

Get started with Spring Data JPA through the guided reference course:

>> CHECK OUT THE COURSE

Partner – Moderne – NPI EA (cat=Spring Boot)
announcement - icon

Refactor Java code safely — and automatically — with OpenRewrite.

Refactoring big codebases by hand is slow, risky, and easy to put off. That’s where OpenRewrite comes in. The open-source framework for large-scale, automated code transformations helps teams modernize safely and consistently.

Each month, the creators and maintainers of OpenRewrite at Moderne run live, hands-on training sessions — one for newcomers and one for experienced users. You’ll see how recipes work, how to apply them across projects, and how to modernize code with confidence.

Join the next session, bring your questions, and learn how to automate the kind of work that usually eats your sprint time.

Course – LJB – NPI EA (cat = Core Java)
announcement - icon

Code your way through and build up a solid, practical foundation of Java:

>> Learn Java Basics

Partner – LambdaTest – NPI EA (cat= Testing)
announcement - icon

Distributed systems often come with complex challenges such as service-to-service communication, state management, asynchronous messaging, security, and more.

Dapr (Distributed Application Runtime) provides a set of APIs and building blocks to address these challenges, abstracting away infrastructure so we can focus on business logic.

In this tutorial, we'll focus on Dapr's pub/sub API for message brokering. Using its Spring Boot integration, we'll simplify the creation of a loosely coupled, portable, and easily testable pub/sub messaging system:

>> Flexible Pub/Sub Messaging With Spring Boot and Dapr

eBook – Java Concurrency – NPI (cat=Java Concurrency)
announcement - icon

Handling concurrency in an application can be a tricky process with many potential pitfalls. A solid grasp of the fundamentals will go a long way to help minimize these issues.

Get started with understanding multi-threaded applications with our Java Concurrency guide:

>> Download the eBook

1. Introduction

Virtual threads are a useful feature officially introduced in JDK 21 as a solution to improve the performance of high-throughput applications.

However, the JDK has no built-in task-scheduling tool that uses virtual threads. Thus, we must write our task scheduler, which runs using virtual threads.

In this article, we’ll create custom schedulers for virtual threads using the Thread.sleep() method and the ScheduledExecutorService class.

2. What Are Virtual Threads?

The Virtual thread was introduced in JEP-444 as a lightweight version of the Thread class that ultimately improves concurrency in high-throughput applications.

Virtual threads use much less space than usual operating system threads (or platform threads). Hence, we can spawn more virtual threads simultaneously in an application than platform threads. Undoubtedly, this increases the maximum number of concurrent units, which also increases the throughput of our applications.

One crucial point is that virtual threads are not faster than platform threads. In our applications, they only appear in larger quantities than platform threads so that they can execute more parallel work.

Virtual threads are cheap, so we don’t need to use techniques like resource pooling to schedule tasks to a limited number of threads. Instead, we can spawn them almost infinitely in modern computers without having memory issues.

Finally, virtual threads are dynamic, whereas platform threads are fixed in size. Thus, virtual threads are much more suitable than platform threads for small tasks such as simple HTTP or database calls.

3. Scheduling Virtual Threads

We’ve seen that one big advantage of virtual threads is that they are small and cheap. We can effectively spawn hundreds of thousands of virtual threads in a simple machine without falling into out-of-memory errors. Thus, pooling virtual threads as we do with more expensive resources like platform threads and network or database connections doesn’t make much sense.

By keeping thread pools, we create another overhead of pooling tasks for available threads in the pool, which is more complex and potentially slower. Additionally, most thread pools in Java are limited by the number of platform threads, which is always smaller than the possible number of virtual threads in the program.

Thus, we must avoid using virtual threads with thread-pooling APIs like ForkJoinPool or ThreadPoolExecutor. Instead, we should always create a new virtual thread for each task.

Currently, Java doesn’t provide a standard API to schedule virtual threads as we do with other concurrent APIs like the ScheduledExecutorService’s schedule() method. So, to effectively make our virtual threads run scheduled tasks, we need to write our own scheduler.

3.1. Scheduling Virtual Threads Using Thread.sleep()

The most straightforward approach we’ll see to create a customized scheduler uses the Thread.sleep() method to make the program wait on the current thread execution:

static Future<?> schedule(Runnable task, int delay, TemporalUnit unit, ExecutorService executorService) {
    return executorService.submit(() -> {
        try {
            Thread.sleep(Duration.of(delay, unit));
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }

        task.run();
    });
}

The schedule() method receives a task to be scheduled, a delay, and an ExecutorService. Then, we fire up the task using ExecutorService‘s submit(). In the try block, we make the thread that will execute the task to wait for a desired delay by calling Thread.sleep(). Hence, the thread may be interrupted while waiting, so we handle the InterruptedException by interrupting the current thread execution.

Finally, after waiting, we call run() with the task received.

To schedule virtual threads with the custom schedule() method, we need to pass an executor service for virtual threads to it:

ExecutorService virtualThreadExecutor = Executors.newVirtualThreadPerTaskExecutor();

try (virtualThreadExecutor) {
    var taskResult = schedule(() -> 
      System.out.println("Running on a scheduled virtual thread!"), 5, ChronoUnit.SECONDS,
      virtualThreadExecutor);

    try {
        Thread.sleep(10 * 1000); // Sleep for 10 seconds to wait task results
    } catch (InterruptedException e) {
        Thread.currentThread()
          .interrupt();
    }

    System.out.println(taskResult.get());
}

Firstly, we instantiate an ExecutorService that spawns a new virtual thread per task we submit. Then, we wrap the virtualThreadExecutor variable in a try-with-resources statement, keeping the executor service open until we finish using it. Alternatively, after using the executor service, we can finish it properly using shutdown().

We call schedule() to run the task after 5 seconds, then wait 10 seconds before trying to get the task execution result.

3.2. Scheduling Virtual Threads Using SingleThreadExecutor

We saw how to use sleep() to schedule tasks to virtual threads. Alternatively, we can instantiate a new single-thread scheduler in the virtual thread executor for each task submitted:

static Future<?> schedule(Runnable task, int delay, TimeUnit unit, ExecutorService executorService) {
    return executorService.submit(() -> {
        ScheduledExecutorService singleThreadScheduler = Executors.newSingleThreadScheduledExecutor();

        try (singleThreadScheduler) {
            singleThreadScheduler.schedule(task, delay, unit);
        }
    });
}

The code also uses a virtual thread ExecutorService passed as an argument to submit tasks. But now, for each task, we instantiate a single ScheduledExecutorService of a single thread using the newSingleThreadScheduledExecutor() method.

Then, inside a try-with-resources block, we schedule tasks using the single thread executor schedule() method. That method accepts a task and delay amount as arguments and doesn’t throw checked InterruptedException like sleep().

Finally, we can schedule tasks to a virtual thread executor using schedule():

ExecutorService virtualThreadExecutor = Executors.newVirtualThreadPerTaskExecutor();

try (virtualThreadExecutor) {
    var taskResult = schedule(() -> 
      System.out.println("Running on a scheduled virtual thread!"), 5, TimeUnit.SECONDS,
      virtualThreadExecutor);

    try {
        Thread.sleep(10 * 1000); // Sleep for 10 seconds to wait task results
    } catch (InterruptedException e) {
        Thread.currentThread()
          .interrupt();
    }

    System.out.println(taskResult.get());
}

This is similar to the usage of the schedule() method of Section 3.1, but here, we pass a TimeUnit instead of ChronoUnit.

3.3. Scheduling Tasks Using sleep() vs. Scheduled Single Thread Executor

In the sleep() scheduling approach, we just called a method to wait before effectively running the task. Thus, it’s straightforward to understand what the code is doing, and it’s easier to debug it. On the other hand, using a scheduled executor service per task depends on the library’s scheduler code, so it might be harder to debug or troubleshoot.

Additionally, if we choose to use sleep(), we’re limited to scheduling tasks to run after a fixed delay. In contrast, using ScheduledExecutorService, we have access to three scheduling methods: schedule()scheduleAtFixedRate(), and scheduleWithFixedDelay().

The ScheduledExecutorService’s schedule() method adds a delay, just like sleep() would. The scheduleAtFixedRate() and scheduleWithFixedDelay() methods add periodicity to the scheduling so we can repeat task execution in fixed-size periods. Therefore, we have more flexibility in scheduling tasks using the ScheduledExecutorService built-in Java library.

4. Conclusion

In this article, we’ve presented a few advantages of using virtual threads over traditional platform threads. Then, we looked at using Thread.sleep() and ScheduledExecutorService to schedule tasks to run in virtual threads.

The code backing this article is available on GitHub. Once you're logged in as a Baeldung Pro Member, start learning and coding on the project.
Baeldung Pro – NPI EA (cat = Baeldung)
announcement - icon

Baeldung Pro comes with both absolutely No-Ads as well as finally with Dark Mode, for a clean learning experience:

>> Explore a clean Baeldung

Once the early-adopter seats are all used, the price will go up and stay at $33/year.

eBook – HTTP Client – NPI EA (cat=HTTP Client-Side)
announcement - icon

The Apache HTTP Client is a very robust library, suitable for both simple and advanced use cases when testing HTTP endpoints. Check out our guide covering basic request and response handling, as well as security, cookies, timeouts, and more:

>> Download the eBook

eBook – Java Concurrency – NPI EA (cat=Java Concurrency)
announcement - icon

Handling concurrency in an application can be a tricky process with many potential pitfalls. A solid grasp of the fundamentals will go a long way to help minimize these issues.

Get started with understanding multi-threaded applications with our Java Concurrency guide:

>> Download the eBook

eBook – Java Streams – NPI EA (cat=Java Streams)
announcement - icon

Since its introduction in Java 8, the Stream API has become a staple of Java development. The basic operations like iterating, filtering, mapping sequences of elements are deceptively simple to use.

But these can also be overused and fall into some common pitfalls.

To get a better understanding on how Streams work and how to combine them with other language features, check out our guide to Java Streams:

>> Join Pro and download the eBook

eBook – Persistence – NPI EA (cat=Persistence)
announcement - icon

Working on getting your persistence layer right with Spring?

Explore the eBook

Course – LS – NPI EA (cat=REST)

announcement - icon

Get started with Spring Boot and with core Spring, through the Learn Spring course:

>> CHECK OUT THE COURSE

Partner – Moderne – NPI EA (tag=Refactoring)
announcement - icon

Modern Java teams move fast — but codebases don’t always keep up. Frameworks change, dependencies drift, and tech debt builds until it starts to drag on delivery. OpenRewrite was built to fix that: an open-source refactoring engine that automates repetitive code changes while keeping developer intent intact.

The monthly training series, led by the creators and maintainers of OpenRewrite at Moderne, walks through real-world migrations and modernization patterns. Whether you’re new to recipes or ready to write your own, you’ll learn practical ways to refactor safely and at scale.

If you’ve ever wished refactoring felt as natural — and as fast — as writing code, this is a good place to start.

eBook – Java Concurrency – NPI (cat=Java Concurrency)
announcement - icon

Handling concurrency in an application can be a tricky process with many potential pitfalls. A solid grasp of the fundamentals will go a long way to help minimize these issues.

Get started with understanding multi-threaded applications with our Java Concurrency guide:

>> Download the eBook

eBook Jackson – NPI EA – 3 (cat = Jackson)