Virtual Threads in Java 21: Simplified Concurrency for Modern Applications
With Java 21, Virtual Threads have redefined how we approach concurrency, offering a lightweight and efficient way to handle parallel and asynchronous tasks. Unlike CompletableFuture or ParallelStream, Virtual Threads allow developers to write simple, synchronous-looking code while achieving the scalability of asynchronous solutions.
Key Features of Virtual Threads:
Example: Virtual Threads with HTTP Requests
Here’s how you can use Virtual Threads to fetch posts concurrently:
Example: Fetch Posts Using Virtual Threads
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.concurrent.*;
public class VirtualThreadExample {
public static void main(String[] args) {
try (ExecutorService myExecutor = Executors.newVirtualThreadPerTaskExecutor()) {
// List of posts to process
List<Integer> posts = List.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20);
long start = System.nanoTime();
// Submit a task for each post
List<Future<Object>> futures = posts.stream()
.map(post -> myExecutor.submit(() -> {
getResponse("https://jsonplaceholder.typicode.com/posts/" + post);
return null; // Explicitly return null for Future<Void>
}))
.toList();
// Wait for all tasks to complete
for (Future<Object> future : futures) {
future.get(); // Ensures task completion
}
long duration = (System.nanoTime() - start) / 1_000_000;
System.out.printf("Processed %d posts in %d millis%n", posts.size(), duration);
System.out.println("Program Completed !!");
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
}
public static void getResponse(String urlRest) {
try {
InputStream is = getInputStream(urlRest);
BufferedReader rd = new BufferedReader(new InputStreamReader(is, StandardCharsets.UTF_8));
StringBuilder response = new StringBuilder();
String line;
while ((line = rd.readLine()) != null) {
response.append(line);
}
rd.close();
System.out.println("Response: " + response.toString().trim());
} catch (Exception e) {
e.printStackTrace();
}
}
private static InputStream getInputStream(String urlRest) throws IOException {
URL url = new URL(urlRest);
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
connection.setRequestMethod("GET");
connection.setRequestProperty("Content-Type", "application/json");
connection.setRequestProperty("Content-Language", "en-US");
connection.setConnectTimeout(60000);
connection.setUseCaches(false);
connection.setDoInput(true);
connection.setDoOutput(true);
return connection.getResponseCode() >= 400
? connection.getErrorStream()
: connection.getInputStream();
}
}
Performance and Simplicity
Output:
Processed 20 posts in 971 millis
Program Completed !!
Example: Asynchronous Task with CompletableFuture.runAsync()
Virtual Threads also work seamlessly with CompletableFuture, enabling fire-and-forget asynchronous tasks. Here’s an example of saving user data asynchronously:
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executors;
import java.util.concurrent.Executor;
public class AsyncExample {
static class UserRepository {
public void saveUser(String user) {
System.out.println("Saving user: " + user);
try {
Thread.sleep(1000); // Simulate database latency
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new RuntimeException("Thread interrupted while saving user", e);
}
System.out.println("User saved: " + user);
}
}
public static void main(String[] args) {
Executor virtualThreadExecutor = Executors.newVirtualThreadPerTaskExecutor();
String user = "JohnDoe";
// Run the task asynchronously
CompletableFuture.runAsync(() -> {
System.out.println("Starting async task for user: " + user);
UserRepository repository = new UserRepository();
repository.saveUser(user);
System.out.println("Async task completed for user: " + user);
}, virtualThreadExecutor)
.exceptionally(ex -> {
System.err.println("Failed to save user: " + user);
ex.printStackTrace();
return null;
});
System.out.println("Main thread continues executing...");
try {
Thread.sleep(2000); // Simulate main thread work
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Main thread finished.");
}
}
Output:
Main thread continues executing...
Starting async task for user: JohnDoe
Saving user: JohnDoe
User saved: JohnDoe
Async task completed for user: JohnDoe
Main thread finished.
Scalability and Readability
Automatic Resource Management
The ExecutorService created using Executors.newVirtualThreadPerTaskExecutor() in the try-with-resources block will automatically be shut down when the block is exited. This is because ExecutorService implements the AutoCloseable interface, and in a try-with-resources block, the close() method is automatically called.
If you're using the ExecutorService outside of a try-with-resources block, you must explicitly shut it down. This ensures that all threads are properly terminated and resources are released.
Why Virtual Threads?
领英推荐
With Virtual Threads in Java 21, you do not need to manually define the number of threads. The JVM efficiently handles scheduling and mapping them to a small pool of carrier threads. Manually limiting Virtual Threads is not a good practice and can negate their benefits.
If you’re still on Java 8 or 17, you might use CompletableFuture or ParallelStream. But with Java 21, Virtual Threads are the most straightforward and efficient way to handle concurrent tasks. Whether you’re making HTTP requests, processing data, or performing database operations, Virtual Threads simplify concurrency while delivering exceptional performance.