DEV Community

realNameHidden
realNameHidden

Posted on

How Virtual Threads Change the Way We Write Concurrent Java Code

For decades, Java programming has been like running a high-end restaurant where every single waiter (Thread) is tied to exactly one table. If the guests at Table 5 are taking twenty minutes to decide on their appetizers, that waiter just stands there, staring into space, unable to help anyone else.

In technical terms, traditional Java threads are wrappers around Operating System (OS) threads. These are "expensive" resources. If you create too many, your server runs out of memory; if they sit idle waiting for a database response, you’re wasting money.

Enter Virtual Threads (introduced in Java 21 via Project Loom). They are the "Super-Waiters" of the Java world. A virtual thread doesn't stay tied to a table. If a guest is dawdling, the virtual thread simply hops away to serve another table and returns the split-second the guest is ready to order.


Core Concepts: What are Virtual Threads?

Virtual Threads are lightweight threads that are not managed by the OS, but by the Java Virtual Machine (JVM). This shift changes everything you know about learn Java concurrency.

Why are they a big deal?

  • Low Overhead: You can literally start millions of virtual threads on a standard laptop. A traditional thread takes about 1MB of memory; a virtual thread takes only a few kilobytes.
  • Non-Blocking Simplicity: Previously, to handle many tasks, we had to use "Reactive Programming" (like WebFlux), which is notoriously hard to read. Virtual threads allow you to write simple, synchronous-looking code that performs like high-end asynchronous code.
  • Scaling the "Thread-per-Request" Model: You no longer need complex thread pools for standard web tasks. You just give every request its own thread.

Use Cases

  1. High-throughput Web Servers: Handling thousands of concurrent API calls.
  2. Blocking I/O Tasks: Waiting for database queries, file reads, or external API responses.
  3. Microservices: Where services spend most of their time waiting for other services to respond.

Code Examples: Java 21 in Action

To run these examples, ensure you are using Java 21 or later.

1. Creating a Million Threads

In the old days, this code would crash your computer with an OutOfMemoryError. With virtual threads, it finishes in seconds.

import java.time.Duration;
import java.util.concurrent.Executors;
import java.util.stream.IntStream;

public class MillionThreadsApp {
    public static void main(String[] args) {
        System.out.println("Starting task...");

        // Use a Virtual Thread Per Task Executor
        try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
            IntStream.range(0, 1_000_000).forEach(i -> {
                executor.submit(() -> {
                    // Simulate a small I/O delay (like a DB call)
                    Thread.sleep(Duration.ofSeconds(1));
                    return i;
                });
            });
        } // Executor automatically closes and waits for all tasks to finish

        System.out.println("Finished 1 million tasks effortlessly!");
    }
}

Enter fullscreen mode Exit fullscreen mode

2. Spring Boot 3 + Virtual Threads (The Controller)

Modern Spring Boot makes it easy to enable virtual threads. In your application.properties, just add: spring.threads.virtual.enabled=true.

Here is a complete REST example:

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestClient;

@RestController
public class VirtualThreadController {

    private final RestClient restClient = RestClient.create();

    @GetMapping("/process-data")
    public String handleRequest() {
        // This looks like blocking code, but on a Virtual Thread, 
        // the underlying OS thread is released during the 'get' call!
        String response = restClient.get()
                .uri("https://jsonplaceholder.typicode.com/posts/1")
                .retrieve()
                .body(String.class);

        return "Data retrieved via Virtual Thread: " + response;
    }
}

Enter fullscreen mode Exit fullscreen mode

Test the Endpoint

Request:

curl -i http://localhost:8080/process-data

Enter fullscreen mode Exit fullscreen mode

Response:

HTTP/1.1 200 OK
Content-Type: text/plain
Data retrieved via Virtual Thread: { "userId": 1, "id": 1, ... }

Enter fullscreen mode Exit fullscreen mode

Best Practices for Virtual Threads

  1. Don't Pool Virtual Threads: We use thread pools for traditional threads because they are expensive to create. Virtual threads are cheap. Just create a new one whenever you need it!
  2. Avoid "Pinning": If you use synchronized blocks or native methods, the virtual thread might get "pinned" to the OS thread, preventing it from hopping away. Use ReentrantLock instead of synchronized for heavy I/O sections.
  3. Keep them for I/O, not CPU-bound tasks: If your task is doing heavy math (CPU-intensive), virtual threads won't help. They are designed for tasks that wait (I/O-bound).
  4. Use Thread Locals Sparingly: Since you might have millions of threads, using ThreadLocal can add up to a massive amount of memory.

Conclusion

Virtual Threads are arguably the most significant update to the Java ecosystem in a decade. They allow us to write easy-to-read, synchronous code that scales to levels previously only possible with complex reactive frameworks. If you are looking to learn Java or upgrade your existing microservices, moving to Java 21 to leverage these threads is a no-brainer.

For a deeper dive into the technical specifications, I highly recommend checking out the Oracle JEP 444 documentation or the Spring Framework Blog.

Top comments (0)