CodeGym /Courses /JAVA 25 SELF /Using Executor with virtual threads

Using Executor with virtual threads

JAVA 25 SELF
Level 57 , Lesson 3
Available

1. The essentials

You are already somewhat familiar with classes from the java.util.concurrent package, especially ExecutorService. It’s a “task manager”: you submit work to it (for example, via submit()), and it decides when and which thread will run it. Usually, there is a fixed-size thread pool under the hood that saves resources and doesn’t create a new thread for every task.

However, virtual threads change the game! Now you can afford a luxury: one dedicated thread per task, without worrying that the JVM will “burst” from overeating.

A new way: Executors.newVirtualThreadPerTaskExecutor()

In Java 21, a new way appeared to create an ExecutorService that runs each task in its own virtual thread:

ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor();

Key difference:

  • Old thread pools (Executors.newFixedThreadPool, Executors.newCachedThreadPool) limited the number of concurrent tasks due to the high cost of OS threads.
  • The new virtual Executor is almost unrestricted: each task gets its own lightweight virtual thread.

Simple example

Let’s submit 10 tasks to the virtual Executor:

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class VirtualExecutorDemo {
    public static void main(String[] args) {
        ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor();

        for (int i = 1; i <= 10; i++) {
            int taskId = i; // capture the variable for the lambda
            executor.submit(() -> {
                System.out.println("Task " + taskId + " is running in thread: " +
                        Thread.currentThread());
            });
        }

        executor.shutdown();
    }
}

What happens?
Each task will be launched in its own virtual thread, and you will see lines like:

Task 1 is running in thread: VirtualThread[#24]/runnable@ForkJoinPool-1-worker-1
...

2. Massive parallelism: thousands of tasks are no problem!

To really feel the power of virtual threads, let’s try submitting not 10 but, say, 100_000 tasks to the ExecutorService. In classic pools this would be like trying to fit an elephant into a fridge: the JVM would quickly run out of memory or start to crawl. With virtual threads, it’s different!

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class VirtualExecutorMassiveDemo {
    public static void main(String[] args) {
        ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor();

        for (int i = 1; i <= 100_000; i++) {
            int taskId = i;
            executor.submit(() -> {
                // For example — just sleep for 1 ms
                try {
                    Thread.sleep(1);
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
                // System.out.println("Task " + taskId + " done."); // Don't print; otherwise there will be way too many lines!
            });
        }

        executor.shutdown();
    }
}

Note: printing 100_000 lines is a bad idea: the console will “choke” much faster than virtual threads. Either don’t print to the console or print only the first few tasks.

3. How newVirtualThreadPerTaskExecutor works

In short: this ExecutorService creates a new virtual thread for every task you submit. Unlike a fixed pool, there’s no task queue and no hard limit on the number of concurrent threads (beyond your JVM and hardware limits).

Architecturally:

  • Virtual threads are mapped onto a small pool of real OS (carrier) threads.
  • The JVM itself decides when and which virtual thread to run, suspend, and resume.
  • If a thread blocks (for example, reading a file or waiting on the network), the JVM can park the virtual thread and free the carrier thread for other tasks.

4. Example: handling results with Future

An ExecutorService returns a Future if a task returns a result. It works just like with regular threads:

import java.util.concurrent.*;

public class VirtualExecutorWithResult {
    public static void main(String[] args) throws InterruptedException, ExecutionException {
        ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor();

        Future<String> future = executor.submit(() -> {
            Thread.sleep(500);
            return "Hello from virtual thread!";
        });

        System.out.println("Result: " + future.get()); // Wait for the result

        executor.shutdown();
    }
}

All is familiar: you can submit tasks that return a value, wait for the result via get(), and exceptions are handled in the standard way.

5. How to shut down the Executor properly

It’s very important not to forget to shut down the ExecutorService, so the program doesn’t hang (even if the threads are virtual rather than “real”).

shutdown() and awaitTermination

executor.shutdown(); // Tell it: we’re not accepting more tasks
executor.awaitTermination(1, TimeUnit.MINUTES); // Wait for all tasks to finish (up to 1 minute)

Why does this matter?
If you don’t call shutdown(), virtual threads may keep living, and the program won’t exit even after main() completes. This is a common beginner mistake.

6. Useful details

Comparison: virtual Executor vs classic thread pool

Classic pool (newFixedThreadPool) Virtual Executor (newVirtualThreadPerTaskExecutor)
Number of threads Limited by pool size One virtual thread per task, almost unlimited
Tasks in a queue Yes, if all threads are busy Generally no: a task gets a thread immediately
Thread cost High (stack, OS resources) Very low (JVM scheduling)
Scalability Limited Almost unlimited
Best suited for CPU-bound tasks, limited parallelism I/O-bound tasks, massive parallelism

Integration with web servers

Modern web servers (for example, Tomcat, Jetty, Undertow) are beginning to support virtual threads. This means you can handle each HTTP request in a separate virtual thread without “choking” under user surges.

Advantage: you don’t need complex asynchronous schemes with callbacks and CompletableFuture; the code becomes simpler — you can write familiar blocking code, yet the application still scales.

Mass testing and load simulation

Virtual threads are a great fit for tests where you need to simulate thousands of concurrent users, requests, or operations. For example, a test that sends 10_000 parallel requests to a server, each in its own virtual thread.

Parallel processing of files and network connections

If your application works with many files or network connections, you can handle each connection in a separate virtual thread without worrying about manually managing pools.

7. Common mistakes when working with virtual Executors

Mistake #1: forgot to call shutdown(). If you don’t close the Executor, the program won’t exit — virtual threads will still be waiting for new tasks. Add awaitTermination(...) if needed.

Mistake #2: using virtual threads for heavy computations. Virtual threads don’t speed up tasks that fully load the CPU. For CPU-bound work, use a fixed pool (Executors.newFixedThreadPool) and tune its size carefully.

Mistake #3: ignoring exceptions inside tasks. If a task throws an exception, it won’t reach the main thread — handle it via Future (the get() method) or via try/catch inside the lambda.

Mistake #4: mixing up the old and new syntax/JDK version. Make sure you’re using the correct JDK version (Java 21+) and that your IDE is configured for virtual thread support. The specific method is Executors.newVirtualThreadPerTaskExecutor().

Mistake #5: relying on ThreadLocal for context propagation. Virtual threads are often created and destroyed; ThreadLocal may not behave as you expect. For context propagation, use ScopedValue (Scoped Values; more details — in the next lecture).

1
Task
JAVA 25 SELF, level 57, lesson 3
Locked
Data transmission from an interplanetary probe 🛰️
Data transmission from an interplanetary probe 🛰️
1
Task
JAVA 25 SELF, level 57, lesson 3
Locked
Monitoring of the robotic manipulator assembly line 🦾
Monitoring of the robotic manipulator assembly line 🦾
Comments
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION