Java Virtual?Threads
Luis Soares, M.Sc.
Lead Software Engineer | Blockchain & ZK Protocol Engineer | ?? Rust | C++ | Web3 | Solidity | Golang | Cryptography | Author
Java introduced a new concept of Virtual Threads as part of Project Loom, an effort to improve concurrency in Java applications. This guide explains the Virtual Threads in Java, their purpose, and their benefits. It will also walk you through examples of how to use them.
What are Virtual Threads?
Virtual Threads (previously known as ‘fibers’) are lightweight, user-mode threads managed entirely by the Java runtime, not by the operating system. They are cheap to create and easy to manage, and they help developers to write more straightforward and efficient concurrent code.
Virtual Threads are designed to make it easier for software developers to write concurrent applications by adding a simpler concurrency model that’s easier to understand and reason about yet is very efficient in terms of system resources.
This is a significant departure from the traditional model of one operating system thread per Java thread, where creating thousands or millions of threads can lead to substantial resource usage and performance problems.
Why Use Virtual Threads?
Virtual Threads are extremely lightweight. You can create millions of virtual threads in a single JVM, which is not feasible with traditional threads due to high memory overhead. They also allow developers to use a simpler programming model based on blocking I/O and locks, reducing the complexity of non-blocking I/O and synchronization.
Creating Virtual Threads in Java
Virtual Threads can be created in Java using the Thread.startVirtualThread method. Here is an example:
Thread.startVirtualThread(() -> {
System.out.println("Hello, World from a Virtual Thread!");
});
This will start a new virtual thread that executes the provided Runnable. Unlike traditional threads, note that there is no need to call a start() method.
Example: Multiple Virtual Threads
Here is an example where we create 10,000 virtual threads:
for (int i = 0; i < 10_000; i++) {
final int index = i;
Thread.startVirtualThread(() -> {
System.out.println("Hello, World from Virtual Thread " + index + "!");
});
}
This will start 10,000 virtual threads. Each Thread will print a message to the console. This would be highly resource-intensive with traditional threads, but it is easily feasible with virtual threads.
Working with Virtual Threads and Executors
You can also use an Executor to manage a pool of virtual threads. Here is an example:
var executor = Executors.newVirtualThreadExecutor();
executor.execute(() -> {
System.out.println("Hello, World from a Virtual Thread via Executor!");
});
executor.close();
In this example, the newVirtualThreadExecutor method returns an Executor that manages a pool of virtual threads. We then use the execute method to run a task on one of the virtual threads.
Blocking Calls in Virtual Threads
One of the advantages of virtual threads is that they handle blocking calls much better than traditional threads. In the following example, the virtual Thread will block for 5 seconds:
Thread.startVirtualThread(() -> {
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Hello, World from a Virtual Thread after sleeping!");
});
With traditional threads, blocking a thread like this could lead to performance problems on a large scale, as each Thread consumes significant system resources. But with virtual threads, blocking costs are much lower, making it feasible to use more straightforward and intuitive blocking-style code even in highly concurrent applications.
Managing Virtual Threads
In the previous examples, we did not keep a reference to the virtual threads we created, so we could not manage them directly after creation. But you can also keep a reference to a virtual thread and use it to manage the Thread:
领英推荐
Thread thread = Thread.startVirtualThread(() -> {
System.out.println("Hello, World from a Managed Virtual Thread!");
});
// Call methods on the thread reference...
Once you have a reference to the Thread, you can call methods on it just like with a traditional thread. For example, you can check if the Thread is alive, wait for it to finish using the join method, interrupt it, etc.
Handling Exceptions in Virtual Threads
Exceptions in virtual threads can be handled in the same way as in traditional threads. Uncaught exceptions will terminate the Thread, but you can catch exceptions in the run method to handle them:
Thread.startVirtualThread(() -> {
try {
// Code that might throw an exception
} catch (Exception e) {
// Handle the exception
}
});
You can also set an uncaught exception handler for a virtual thread:
Thread thread = Thread.startVirtualThread(() -> {
// Code that might throw an exception
});
thread.setOnUncaughtExceptionHandler((t, e) -> {
// Handle the exception
});
Differences Between Virtual and Regular Threads
While the programming model for virtual threads is very similar to that for regular threads, there are some differences:
Monitoring and Debugging Virtual Threads
Java’s built-in monitoring and debugging tools have been updated to work with virtual threads. This includes the jstack tool for examining stack traces, the jstat tool for statistics, and the JMX API for monitoring and managing Java applications.
The ThreadMXBean interface, used for thread monitoring, has been updated to include methods for working with virtual threads. For example, you can use the getVirtualThreadCount method to get the number of live virtual threads, similar to how you would use the getThreadCount method for traditional threads.
Debugging virtual threads with a standard debugger should work as expected. Breakpoints, step-through execution, and inspection of variables should all function normally.
Working with IO in Virtual Threads
When working with IO, blocking operations in virtual threads will not block the underlying kernel thread, allowing other virtual threads to continue running. This means you can use simple, straightforward blocking IO code without worrying about thread pool exhaustion.
This contrasts with traditional threads, where blocking IO can lead to thread starvation and decreased application throughput. As a result, many applications have turned to non-blocking IO, which can be more complex to work with.
Stay tuned, and happy coding!
Visit my Blog for more articles, news, and software engineering stuff!
All the best,
Luis Soares
Senior Java Engineer | Tech Lead | AWS Solutions Architect | Rust | Golang | Java | TypeScript | Web3 & Blockchain
Scalable Systems & Microservices, Cloud and Databases
6 个月??