CodeGym /Courses /JAVA 25 SELF /Creating virtual threads: Thread.ofVirtual().start()

Creating virtual threads: Thread.ofVirtual().start()

JAVA 25 SELF
Level 57 , Lesson 1
Available

1. How to create virtual threads in practice

Time to move from theory to practice! You already know how a thread is created the classic way:

Thread t = new Thread(() -> System.out.println("Hello from thread!"));
t.start();

Or a bit shorter:

new Thread(() -> System.out.println("Hi!")).start();

Now with Java 21 we have a new way:

Thread.startVirtualThread(() -> System.out.println("Hello from virtual thread!"));

or more explicitly:

Thread t = Thread.ofVirtual().start(() -> System.out.println("Hello from virtual thread!"));

What’s the difference?

  • Thread.ofVirtual().start(...) creates a virtual thread (Virtual Thread) managed by the JVM, not the OS.
  • Thread.ofPlatform().start(...) (or new Thread(...)) — a classic platform thread, like before.

Why does it matter?

You can create tens of thousands of virtual threads without fearing OutOfMemoryError. Now, if you suddenly decide to handle a million requests — Java will say: “No problem, bring it on!”

2. Syntax for creating a virtual thread

Basic example:

public class VirtualThreadDemo {
    public static void main(String[] args) {
        Thread thread = Thread.ofVirtual().start(() -> {
            System.out.println("Hello from a virtual thread! Thread: " + Thread.currentThread());
        });
        // Wait for the thread to finish (so main doesn't exit earlier)
        try {
            thread.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

What’s happening?

  • We create a virtual thread via Thread.ofVirtual().start(...).
  • Inside the thread there’s a simple action: print a message.
  • At the end we call thread.join() so the main thread waits for the virtual one to finish (otherwise the program may end before the thread prints anything).

Note:
A virtual thread looks and behaves almost like a regular one, but under the hood — JVM magic!

3. Mass creation of virtual threads: the power of Loom in practice

Let’s try something that would be risky (or simply impossible) with regular threads: create 10_000 virtual threads, each printing its number.

public class VirtualThreadMassive {
    public static void main(String[] args) throws InterruptedException {
        int N = 10_000;
        Thread[] threads = new Thread[N];

        for (int i = 0; i < N; i++) {
            int threadNum = i;
            threads[i] = Thread.ofVirtual().start(() -> {
                System.out.println("Virtual thread #" + threadNum + " is running!");
            });
        }

        // Wait for all threads to finish
        for (Thread t : threads) {
            t.join();
        }
        System.out.println("All virtual threads have finished!");
    }
}
  • For regular threads (new Thread(...)) such code will almost certainly “crash” your program with OutOfMemoryError.
  • For virtual threads — this is normal! The JVM will easily handle thousands and tens of thousands of threads.

By the way, if 10_000 seems like a lot, try 100_000 or even 1_000_000. On a modern machine the JVM will cope, provided your threads do simple work or wait for I/O.

4. Runnable and lambdas: how to pass code to a virtual thread

Virtual threads accept tasks just like regular threads: via the Runnable interface. That means you can pass a lambda expression, a method reference, or any object that implements Runnable.

Lambda example:

Thread.ofVirtual().start(() -> System.out.println("Lambda in a virtual thread!"));

Method example:

public class TaskRunner {
    public static void main(String[] args) {
        Thread.ofVirtual().start(TaskRunner::doWork);
    }

    static void doWork() {
        System.out.println("Working in a virtual thread: " + Thread.currentThread());
    }
}

Anonymous class example:

Thread.ofVirtual().start(new Runnable() {
    @Override
    public void run() {
        System.out.println("Anonymous class in a virtual thread!");
    }
});

Takeaway:
Everything that worked with regular threads also works with virtual ones — only now it’s “lightweight and fast.”

5. Comparison with ExecutorService: the old and the new approach

Classic ExecutorService

ExecutorService executor = Executors.newFixedThreadPool(10);
for (int i = 0; i < 100; i++) {
    int taskNum = i;
    executor.submit(() -> {
        System.out.println("Task #" + taskNum + " is running");
    });
}
executor.shutdown();

Problem:
If there are too many tasks and too few threads, the tasks wait in a queue. If there are too many threads, the program will choke due to lack of resources.

New way: an executor on virtual threads

ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor();
for (int i = 0; i < 100_000; i++) {
    int taskNum = i;
    executor.submit(() -> {
        System.out.println("Virtual task #" + taskNum);
    });
}
executor.shutdown();

What’s happening?

  • A separate virtual thread is created for each task.
  • The JVM manages their scheduling by itself without overloading the system.
  • No need to limit the pool size — virtual threads are “almost free.”

When should you use an executor with virtual threads?

  • When you have a large stream of tasks and you don’t want to think about pool size.
  • When tasks are independent and can run in parallel.
  • When you want simplicity: no manual thread management.

6. Practical advice: when to use what

When to use Thread.ofVirtual().start() directly?

  • When you need to create a separate thread for a unique task (for example, for a test, demo, or a simple experiment).
  • When the number of threads is small and you want to manage them manually.

When to use Executors.newVirtualThreadPerTaskExecutor()?

  • When you need to launch tasks at scale (for example, processing a large number of requests, files, or network connections).
  • When tasks are independent and don’t require coordination with each other.
  • When you want to integrate virtual threads into an existing architecture that already uses ExecutorService (for example, in a web server, a task processor, etc.).

Tip:
If you’re unsure — start with an executor on virtual threads. It’s the most versatile and modern approach.

7. Exception handling in virtual threads

Virtual threads are ordinary threads from a try-catch point of view. If an exception occurs inside your Runnable, it won’t crash the whole JVM; it will simply terminate that thread with an error.

Example:

Thread t = Thread.ofVirtual().start(() -> {
    throw new RuntimeException("Something went wrong!");
});
try {
    t.join();
} catch (InterruptedException e) {
    e.printStackTrace();
}
System.out.println("The main thread continued working.");

In ExecutorService:
If you submit a task via submit, you can get the result via Future, and the exception will be rethrown when calling get():

ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor();
Future<?> f = executor.submit(() -> {
    throw new RuntimeException("Error in a virtual task");
});
try {
    f.get();
} catch (ExecutionException e) {
    System.out.println("Caught an error from a virtual thread: " + e.getCause());
}
executor.shutdown();

8. Common mistakes when creating virtual threads

Error #1: Confusing virtual and platform threads. If you create threads via new Thread(...) or Thread.ofPlatform(), these are not virtual threads. Only Thread.ofVirtual().start(...) or methods from Executors give you real virtual threads.

Error #2: Expecting speedups for heavy computations. Virtual threads do not speed up CPU-bound tasks. If you have a million threads, each computing pi to the millionth digit, the JVM won’t “speed up” the computation; it will just switch between threads.

Error #3: Holding resources (for example, databases) per thread. If you create a million virtual threads but each one needs a separate database connection, the database won’t withstand it. Virtual threads are great for tasks where most of the time is waiting (I/O), not working with limited external resources.

Error #4: Not waiting for threads to finish when it matters. If the main thread exits before the virtual threads, the program may finish without waiting for results. Use join() or ExecutorService with shutdown() and awaitTermination().

Error #5: Using outdated libraries incompatible with virtual threads. Some third-party libraries may block OS-level threads or use native synchronization, which reduces the efficiency of virtual threads. Always check compatibility.

1
Task
JAVA 25 SELF, level 57, lesson 1
Locked
Race car telemetry 🏎️
Race car telemetry 🏎️
1
Task
JAVA 25 SELF, level 57, lesson 1
Locked
Managing the experimental helper robot 🤖
Managing the experimental helper robot 🤖
Comments
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION