Introduction to Virtual Threads
Virtual Threads (Project Loom), introduced in Java 19 as a preview and finalized in Java 21, are lightweight threads that dramatically reduce the overhead of thread creation and management. They enable highly concurrent applications without the limitations of platform threads.
1. Basic Virtual Threads Usage
Creating Virtual Threads
import java.util.concurrent.*; public class VirtualThreadsBasic { public static void main(String[] args) throws Exception { // Method 1: Using Thread.startVirtualThread() Thread virtualThread = Thread.startVirtualThread(() -> { System.out.println("Hello from virtual thread: " + Thread.currentThread()); }); virtualThread.join(); // Method 2: Using Thread.ofVirtual() Thread.Builder virtualBuilder = Thread.ofVirtual().name("worker-", 0); Thread vt1 = virtualBuilder.start(() -> { System.out.println("Virtual thread 1: " + Thread.currentThread().getName()); }); Thread vt2 = virtualBuilder.start(() -> { System.out.println("Virtual thread 2: " + Thread.currentThread().getName()); }); vt1.join(); vt2.join(); // Method 3: Using Executors.newVirtualThreadPerTaskExecutor() try (var executor = Executors.newVirtualThreadPerTaskExecutor()) { Future<String> future = executor.submit(() -> { Thread.sleep(1000); return "Result from virtual thread"; }); System.out.println("Future result: " + future.get()); } } } Comparing Platform vs Virtual Threads
import java.util.concurrent.*; import java.util.stream.*; public class ThreadComparison { public static void main(String[] args) throws Exception { int taskCount = 10_000; System.out.println("=== Platform Threads ==="); testWithPlatformThreads(taskCount); System.out.println("\n=== Virtual Threads ==="); testWithVirtualThreads(taskCount); } private static void testWithPlatformThreads(int taskCount) throws Exception { long startTime = System.currentTimeMillis(); try (var executor = Executors.newFixedThreadPool(200)) { var futures = IntStream.range(0, taskCount) .mapToObj(i -> executor.submit(() -> { Thread.sleep(100); // Simulate work return i; })) .toList(); // Wait for all tasks for (var future : futures) { future.get(); } } long duration = System.currentTimeMillis() - startTime; System.out.printf("Completed %d tasks in %d ms%n", taskCount, duration); } private static void testWithVirtualThreads(int taskCount) throws Exception { long startTime = System.currentTimeMillis(); try (var executor = Executors.newVirtualThreadPerTaskExecutor()) { var futures = IntStream.range(0, taskCount) .mapToObj(i -> executor.submit(() -> { Thread.sleep(100); // Simulate work return i; })) .toList(); // Wait for all tasks for (var future : futures) { future.get(); } } long duration = System.currentTimeMillis() - startTime; System.out.printf("Completed %d tasks in %d ms%n", taskCount, duration); } } 2. ExecutorService with Virtual Threads
Basic Virtual Thread Executor
import java.util.concurrent.*; import java.util.*; public class VirtualThreadExecutorBasics { public static void main(String[] args) throws Exception { // 1. Virtual Thread Per Task Executor (most common) try (ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor()) { List<Future<String>> futures = new ArrayList<>(); for (int i = 0; i < 10; i++) { final int taskId = i; Future<String> future = executor.submit(() -> { String threadName = Thread.currentThread().getName(); System.out.printf("Task %d executed by %s%n", taskId, threadName); Thread.sleep(1000); // Simulate I/O operation return "Task-" + taskId + "-completed"; }); futures.add(future); } // Collect results for (Future<String> future : futures) { System.out.println("Result: " + future.get()); } } // 2. Custom Virtual Thread Factory ThreadFactory virtualThreadFactory = Thread.ofVirtual().factory(); try (ExecutorService customExecutor = Executors.newThreadPerTaskExecutor(virtualThreadFactory)) { customExecutor.submit(() -> { System.out.println("Custom virtual thread: " + Thread.currentThread()); }); } } } Advanced Virtual Thread Configuration
import java.util.concurrent.*; import java.util.*; public class AdvancedVirtualThreadExecutor { public static void main(String[] args) throws Exception { // Custom virtual thread factory with configuration ThreadFactory customVirtualThreadFactory = Thread.ofVirtual() .name("custom-vt-", 0) .inheritInheritableThreadLocals(false) // Don't inherit thread locals .factory(); try (ExecutorService executor = Executors.newThreadPerTaskExecutor(customVirtualThreadFactory)) { // Submit multiple tasks List<Callable<String>> tasks = new ArrayList<>(); for (int i = 0; i < 5; i++) { final int taskId = i; tasks.add(() -> { String threadName = Thread.currentThread().getName(); System.out.printf("Task %d on %s%n", taskId, threadName); // Simulate different types of work if (taskId % 2 == 0) { Thread.sleep(500); // I/O bound } else { // CPU-bound work (virtual threads still work, but no benefit) long result = 0; for (int j = 0; j < 1000; j++) { result += j; } } return "Task-" + taskId + "-done"; }); } // Invoke all tasks List<Future<String>> futures = executor.invokeAll(tasks); // Process results for (Future<String> future : futures) { System.out.println("Completed: " + future.get()); } } } } 3. Real-World Use Cases
HTTP Client with Virtual Threads
import java.net.http.*; import java.net.URI; import java.util.concurrent.*; import java.util.*; public class HttpClientVirtualThreads { private final HttpClient httpClient; private final ExecutorService virtualThreadExecutor; public HttpClientVirtualThreads() { this.httpClient = HttpClient.newBuilder() .executor(Executors.newVirtualThreadPerTaskExecutor()) .build(); this.virtualThreadExecutor = Executors.newVirtualThreadPerTaskExecutor(); } public CompletableFuture<String> fetchUrlAsync(String url) { HttpRequest request = HttpRequest.newBuilder() .uri(URI.create(url)) .GET() .build(); return httpClient.sendAsync(request, HttpResponse.BodyHandlers.ofString()) .thenApply(HttpResponse::body); } public List<String> fetchMultipleUrls(List<String> urls) throws Exception { List<Callable<String>> tasks = new ArrayList<>(); for (String url : urls) { tasks.add(() -> { HttpRequest request = HttpRequest.newBuilder() .uri(URI.create(url)) .GET() .build(); HttpResponse<String> response = httpClient.send( request, HttpResponse.BodyHandlers.ofString()); return String.format("URL: %s, Status: %d, Length: %d", url, response.statusCode(), response.body().length()); }); } // Execute all HTTP requests concurrently return virtualThreadExecutor.invokeAll(tasks).stream() .map(future -> { try { return future.get(); } catch (Exception e) { return "Error: " + e.getMessage(); } }) .toList(); } public void shutdown() { virtualThreadExecutor.shutdown(); } public static void main(String[] args) throws Exception { HttpClientVirtualThreads fetcher = new HttpClientVirtualThreads(); List<String> urls = List.of( "https://httpbin.org/delay/1", // Simulates 1-second delay "https://httpbin.org/delay/2", // Simulates 2-second delay "https://httpbin.org/delay/1", "https://httpbin.org/status/200", "https://httpbin.org/status/404" ); long startTime = System.currentTimeMillis(); List<String> results = fetcher.fetchMultipleUrls(urls); long duration = System.currentTimeMillis() - startTime; System.out.printf("Fetched %d URLs in %d ms%n", urls.size(), duration); results.forEach(System.out::println); fetcher.shutdown(); } } Database Operations with Virtual Threads
import java.sql.*; import java.util.concurrent.*; import java.util.*; import java.util.concurrent.atomic.*; public class DatabaseVirtualThreads { private final ExecutorService virtualThreadExecutor; private final AtomicInteger connectionCounter; public DatabaseVirtualThreads() { this.virtualThreadExecutor = Executors.newVirtualThreadPerTaskExecutor(); this.connectionCounter = new AtomicInteger(0); } // Simulate database connection private Connection getConnection() throws SQLException { // In real application, this would be a real database connection System.out.println("Getting connection: " + connectionCounter.incrementAndGet()); Thread.sleep(100); // Simulate connection overhead return null; // Simplified for example } public CompletableFuture<List<Map<String, Object>>> queryUsersAsync(List<Integer> userIds) { List<CompletableFuture<Map<String, Object>>> futures = userIds.stream() .map(userId -> CompletableFuture.supplyAsync(() -> { try { return queryUserById(userId); } catch (SQLException e) { throw new CompletionException(e); } }, virtualThreadExecutor)) .toList(); return CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])) .thenApply(v -> futures.stream() .map(CompletableFuture::join) .toList()); } private Map<String, Object> queryUserById(int userId) throws SQLException { System.out.printf("Querying user %d on virtual thread: %s%n", userId, Thread.currentThread().getName()); // Simulate database I/O Thread.sleep(200); Map<String, Object> user = new HashMap<>(); user.put("id", userId); user.put("name", "User-" + userId); user.put("email", "user" + userId + "@example.com"); user.put("thread", Thread.currentThread().getName()); return user; } public void batchProcessUsers(List<Integer> userIds) throws Exception { List<Callable<String>> tasks = userIds.stream() .map(userId -> (Callable<String>) () -> { try { Map<String, Object> user = queryUserById(userId); // Simulate additional processing Thread.sleep(100); return String.format("Processed user %d: %s", userId, user.get("name")); } catch (Exception e) { return "Failed for user " + userId + ": " + e.getMessage(); } }) .toList(); List<Future<String>> futures = virtualThreadExecutor.invokeAll(tasks); for (Future<String> future : futures) { System.out.println(future.get()); } } public void shutdown() { virtualThreadExecutor.shutdown(); } public static void main(String[] args) throws Exception { DatabaseVirtualThreads processor = new DatabaseVirtualThreads(); // Generate test user IDs List<Integer> userIds = new ArrayList<>(); for (int i = 1; i <= 50; i++) { userIds.add(i); } System.out.println("=== Batch Processing Users ==="); long startTime = System.currentTimeMillis(); processor.batchProcessUsers(userIds); long duration = System.currentTimeMillis() - startTime; System.out.printf("\nProcessed %d users in %d ms%n", userIds.size(), duration); processor.shutdown(); } } 4. Error Handling and Resource Management
Structured Concurrency with Virtual Threads
import java.util.concurrent.*; import java.util.*; public class StructuredConcurrencyVirtualThreads { public static class TaskResult { private final String taskName; private final String result; private final Throwable error; public TaskResult(String taskName, String result, Throwable error) { this.taskName = taskName; this.result = result; this.error = error; } public boolean isSuccess() { return error == null; } public String getTaskName() { return taskName; } public String getResult() { return result; } public Throwable getError() { return error; } } public List<TaskResult> executeTasksWithStructuredConcurrency(List<String> tasks) throws InterruptedException { try (var scope = new StructuredTaskScope.ShutdownOnFailure(); var executor = Executors.newVirtualThreadPerTaskExecutor()) { List<StructuredTaskScope.Subtask<String>> subtasks = new ArrayList<>(); // Submit all tasks for (String task : tasks) { StructuredTaskScope.Subtask<String> subtask = scope.fork(() -> executeTask(task, executor)); subtasks.add(subtask); } try { // Wait for all tasks or first failure scope.join(); scope.throwIfFailed(); // Throws if any task failed } catch (ExecutionException e) { System.out.println("One or more tasks failed: " + e.getMessage()); } // Collect results List<TaskResult> results = new ArrayList<>(); for (StructuredTaskScope.Subtask<String> subtask : subtasks) { if (subtask.state() == StructuredTaskScope.Subtask.State.SUCCESS) { results.add(new TaskResult( "Task", subtask.get(), null )); } else if (subtask.state() == StructuredTaskScope.Subtask.State.FAILED) { results.add(new TaskResult( "Task", null, subtask.exception() )); } } return results; } } private String executeTask(String taskName, ExecutorService executor) { try { // Simulate different types of tasks if (taskName.contains("slow")) { Thread.sleep(2000); } else if (taskName.contains("error")) { throw new RuntimeException("Simulated error for: " + taskName); } else { Thread.sleep(500); } return taskName + "-completed"; } catch (InterruptedException e) { Thread.currentThread().interrupt(); throw new RuntimeException("Task interrupted: " + taskName, e); } } public static void main(String[] args) throws Exception { StructuredConcurrencyVirtualThreads executor = new StructuredConcurrencyVirtualThreads(); List<String> tasks = List.of( "fast-task-1", "fast-task-2", "slow-task-1", "error-task", "fast-task-3" ); List<TaskResult> results = executor.executeTasksWithStructuredConcurrency(tasks); System.out.println("\n=== Task Execution Results ==="); for (TaskResult result : results) { if (result.isSuccess()) { System.out.printf("✓ %s: %s%n", result.getTaskName(), result.getResult()); } else { System.out.printf("✗ %s: %s%n", result.getTaskName(), result.getError().getMessage()); } } } } Resource Cleanup with Virtual Threads
import java.util.concurrent.*; import java.util.*; import java.util.concurrent.locks.*; public class ResourceManagementVirtualThreads { private final ExecutorService virtualThreadExecutor; private final Set<AutoCloseable> resources; private final ReentrantLock resourceLock; public ResourceManagementVirtualThreads() { this.virtualThreadExecutor = Executors.newVirtualThreadPerTaskExecutor(); this.resources = ConcurrentHashMap.newKeySet(); this.resourceLock = new ReentrantLock(); } public <T> CompletableFuture<T> executeWithResource( Callable<T> task, AutoCloseable resource) { // Register resource for cleanup resources.add(resource); return CompletableFuture.supplyAsync(() -> { try { T result = task.call(); return result; } catch (Exception e) { throw new CompletionException(e); } finally { // Always cleanup resource cleanupResource(resource); } }, virtualThreadExecutor); } private void cleanupResource(AutoCloseable resource) { resourceLock.lock(); try { if (resources.remove(resource)) { try { resource.close(); System.out.println("Resource cleaned up: " + resource); } catch (Exception e) { System.err.println("Error cleaning up resource: " + e.getMessage()); } } } finally { resourceLock.unlock(); } } public void shutdown() { // Cleanup any remaining resources resourceLock.lock(); try { for (AutoCloseable resource : resources) { try { resource.close(); } catch (Exception e) { System.err.println("Error during shutdown cleanup: " + e.getMessage()); } } resources.clear(); } finally { resourceLock.unlock(); } virtualThreadExecutor.shutdown(); } // Simulated resource private static class SimulatedResource implements AutoCloseable { private final String name; private volatile boolean closed = false; public SimulatedResource(String name) { this.name = name; } public String readData() throws Exception { if (closed) throw new IllegalStateException("Resource closed"); Thread.sleep(100); // Simulate I/O return "Data from " + name; } @Override public void close() throws Exception { if (!closed) { closed = true; Thread.sleep(50); // Simulate cleanup System.out.println("Closed resource: " + name); } } @Override public String toString() { return "SimulatedResource{name='" + name + "', closed=" + closed + "}"; } } public static void main(String[] args) throws Exception { ResourceManagementVirtualThreads manager = new ResourceManagementVirtualThreads(); List<CompletableFuture<String>> futures = new ArrayList<>(); for (int i = 0; i < 5; i++) { SimulatedResource resource = new SimulatedResource("Resource-" + i); CompletableFuture<String> future = manager.executeWithResource(() -> { String data = resource.readData(); System.out.println("Processed: " + data + " on " + Thread.currentThread().getName()); return data; }, resource); futures.add(future); } // Wait for all tasks CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join(); System.out.println("\nAll tasks completed. Resources should be cleaned up."); manager.shutdown(); } } 5. Performance Monitoring and Tuning
Virtual Threads Monitoring
import java.util.concurrent.*; import java.lang.management.*; import java.util.*; import java.util.concurrent.atomic.*; public class VirtualThreadsMonitoring { private final ExecutorService virtualThreadExecutor; private final ThreadMXBean threadMXBean; private final AtomicInteger tasksCompleted; private final AtomicInteger tasksFailed; public VirtualThreadsMonitoring() { this.virtualThreadExecutor = Executors.newVirtualThreadPerTaskExecutor(); this.threadMXBean = ManagementFactory.getThreadMXBean(); this.tasksCompleted = new AtomicInteger(); this.tasksFailed = new AtomicInteger(); } public void executeMonitoredTasks(int taskCount) throws Exception { List<CompletableFuture<Void>> futures = new ArrayList<>(); printThreadStats("Before task execution"); for (int i = 0; i < taskCount; i++) { final int taskId = i; CompletableFuture<Void> future = CompletableFuture.runAsync(() -> { try { executeMonitoredTask(taskId); tasksCompleted.incrementAndGet(); } catch (Exception e) { tasksFailed.incrementAndGet(); System.err.println("Task " + taskId + " failed: " + e.getMessage()); } }, virtualThreadExecutor); futures.add(future); } // Wait for all tasks CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join(); printThreadStats("After task execution"); printTaskStatistics(taskCount); } private void executeMonitoredTask(int taskId) throws InterruptedException { long startTime = System.currentTimeMillis(); // Simulate mixed I/O and computation if (taskId % 3 == 0) { // I/O intensive Thread.sleep(200); } else if (taskId % 3 == 1) { // Mixed Thread.sleep(100); doSomeComputation(); Thread.sleep(100); } else { // Computation with small I/O doSomeComputation(); Thread.sleep(50); } long duration = System.currentTimeMillis() - startTime; System.out.printf("Task %d completed in %d ms on %s%n", taskId, duration, Thread.currentThread().getName()); } private void doSomeComputation() { // Simulate CPU work long result = 0; for (int i = 0; i < 1000; i++) { result += i * i; } } private void printThreadStats(String phase) { System.out.println("\n=== " + phase + " ==="); System.out.println("Platform threads: " + threadMXBean.getThreadCount()); System.out.println("Peak threads: " + threadMXBean.getPeakThreadCount()); // Virtual thread stats (Java 21+) try { // This would require accessing internal APIs in real implementation System.out.println("Virtual threads: [monitoring not directly available]"); } catch (Exception e) { // Virtual thread monitoring might not be directly available } } private void printTaskStatistics(int totalTasks) { System.out.println("\n=== Task Statistics ==="); System.out.println("Total tasks: " + totalTasks); System.out.println("Completed: " + tasksCompleted.get()); System.out.println("Failed: " + tasksFailed.get()); System.out.println("Success rate: " + (tasksCompleted.get() * 100.0 / totalTasks) + "%"); } public void shutdown() { virtualThreadExecutor.shutdown(); } public static void main(String[] args) throws Exception { VirtualThreadsMonitoring monitor = new VirtualThreadsMonitoring(); try { monitor.executeMonitoredTasks(100); } finally { monitor.shutdown(); } } } 6. Best Practices and Patterns
Virtual Threads Best Practices
import java.util.concurrent.*; import java.util.*; public class VirtualThreadsBestPractices { // 1. Use for I/O-bound tasks, not CPU-bound tasks public void ioBoundWork() throws Exception { try (var executor = Executors.newVirtualThreadPerTaskExecutor()) { List<Callable<String>> ioTasks = List.of( () -> { Thread.sleep(100); return "Database query result"; }, () -> { Thread.sleep(150); return "HTTP API response"; }, () -> { Thread.sleep(200); return "File read completed"; } ); List<Future<String>> results = executor.invokeAll(ioTasks); for (Future<String> result : results) { System.out.println(result.get()); } } } // 2. Avoid synchronized blocks with virtual threads public void useReentrantLockInsteadOfSynchronized() throws Exception { ReentrantLock lock = new ReentrantLock(); try (var executor = Executors.newVirtualThreadPerTaskExecutor()) { List<Callable<String>> tasks = new ArrayList<>(); for (int i = 0; i < 10; i++) { final int taskId = i; tasks.add(() -> { lock.lock(); try { // Critical section Thread.sleep(50); return "Task " + taskId + " completed"; } finally { lock.unlock(); } }); } executor.invokeAll(tasks).forEach(future -> { try { System.out.println(future.get()); } catch (Exception e) { e.printStackTrace(); } }); } } // 3. Use CompletableFuture for complex async workflows public CompletableFuture<String> complexAsyncWorkflow() { try (var executor = Executors.newVirtualThreadPerTaskExecutor()) { return CompletableFuture .supplyAsync(() -> fetchUserData(), executor) .thenApplyAsync(userData -> validateUserData(userData), executor) .thenComposeAsync(validData -> processUserData(validData), executor) .exceptionally(throwable -> { System.err.println("Workflow failed: " + throwable.getMessage()); return "Fallback result"; }); } } // 4. Proper resource cleanup with try-with-resources public void properResourceManagement() throws Exception { // ExecutorService implements AutoCloseable in virtual thread executors try (var executor = Executors.newVirtualThreadPerTaskExecutor()) { executor.submit(() -> { // Simulate work with resources try (var connection = getDatabaseConnection(); var statement = connection.createStatement()) { Thread.sleep(100); System.out.println("Work completed with proper resource management"); } catch (Exception e) { throw new RuntimeException(e); } }); } // Executor automatically shutdown here } // 5. Batch operations for better throughput public void batchOperations(List<String> items) throws Exception { try (var executor = Executors.newVirtualThreadPerTaskExecutor()) { int batchSize = 10; List<List<String>> batches = new ArrayList<>(); for (int i = 0; i < items.size(); i += batchSize) { batches.add(items.subList(i, Math.min(i + batchSize, items.size()))); } List<Callable<String>> batchTasks = batches.stream() .map(batch -> (Callable<String>) () -> processBatch(batch)) .toList(); List<Future<String>> results = executor.invokeAll(batchTasks); for (Future<String> result : results) { System.out.println("Batch result: " + result.get()); } } } // Helper methods private String fetchUserData() { try { Thread.sleep(100); } catch (InterruptedException e) { } return "User data"; } private String validateUserData(String data) { try { Thread.sleep(50); } catch (InterruptedException e) { } if (data == null) throw new IllegalArgumentException("Invalid data"); return data + " (validated)"; } private CompletableFuture<String> processUserData(String data) { return CompletableFuture.supplyAsync(() -> { try { Thread.sleep(100); } catch (InterruptedException e) { } return data + " (processed)"; }, Executors.newVirtualThreadPerTaskExecutor()); } private AutoCloseable getDatabaseConnection() { return new AutoCloseable() { @Override public void close() throws Exception { System.out.println("Connection closed"); } }; } private String processBatch(List<String> batch) { try { Thread.sleep(100); return "Processed batch of " + batch.size() + " items"; } catch (InterruptedException e) { throw new RuntimeException(e); } } public static void main(String[] args) throws Exception { VirtualThreadsBestPractices practices = new VirtualThreadsBestPractices(); System.out.println("=== I/O Bound Work ==="); practices.ioBoundWork(); System.out.println("\n=== Batch Operations ==="); List<String> items = new ArrayList<>(); for (int i = 0; i < 25; i++) { items.add("Item-" + i); } practices.batchOperations(items); System.out.println("\n=== Complex Async Workflow ==="); practices.complexAsyncWorkflow() .thenAccept(result -> System.out.println("Final result: " + result)) .join(); } } Summary
Key Benefits of Virtual Threads:
- Massive Concurrency: Create millions of virtual threads efficiently
- Simplified Code: Write blocking code without thread pool management
- Better Resource Utilization: Optimal for I/O-bound workloads
- Platform Integration: Work with existing Java APIs seamlessly
Best Practices:
- Use for I/O-bound tasks, not CPU-bound computation
- Avoid synchronized blocks - use
ReentrantLockinstead - Use try-with-resources for
ExecutorServicecleanup - Leverage Structured Concurrency for task management
- Monitor and tune based on application needs
Common Use Cases:
- HTTP servers handling many concurrent requests
- Database applications with many concurrent connections
- Microservices with high concurrency requirements
- Batch processing of I/O-intensive tasks
- Async/await patterns with simplified code
Performance Characteristics:
- Low memory footprint (~2KB per virtual thread vs ~1MB per platform thread)
- Fast creation/destruction (microseconds vs milliseconds)
- Efficient I/O operations with automatic yielding
- Compatible with existing code and libraries
Virtual Threads represent a fundamental shift in Java concurrency, making highly concurrent applications much simpler to write and maintain while providing exceptional performance for I/O-bound workloads.