Java 21 introduced Virtual Threads (formerly known as Project Loom fibers) as a lightweight alternative to platform threads, enabling massive concurrency with minimal resource overhead.
1. Virtual Threads Overview
What are Virtual Threads?
- Lightweight threads managed by the JVM
- Not OS threads - mapped to carrier threads
- Very cheap to create (millions possible)
- Automatic scheduling by JVM
- Seamless integration with existing code
Traditional vs Virtual Threads
public class VirtualThreadsOverview { public static void main(String[] args) throws Exception { demonstrateThreadComparison(); showVirtualThreadBenefits(); } public static void demonstrateThreadComparison() { System.out.println("=== Traditional vs Virtual Threads ==="); System.out.println(""" Traditional Platform Threads: - 1:1 mapping to OS threads - Expensive to create (~1MB stack each) - Limited by OS thread limits (~1000-10000) - Blocking operations block OS threads Virtual Threads: - M:N mapping to carrier threads - Cheap to create (~200 bytes each) - Can create millions of virtual threads - Blocking doesn't block carrier threads - Automatic scheduling by JVM """); } public static void showVirtualThreadBenefits() { System.out.println("=== Virtual Thread Benefits ==="); System.out.println(""" Key Advantages: Performance: - Massive concurrency with minimal memory - No context switching overhead - Better CPU utilization Simplicity: - Same programming model as platform threads - Works with existing synchronized blocks - Compatible with java.util.concurrent Scalability: - Handle millions of concurrent operations - Perfect for I/O-bound workloads - Better resource utilization """); } } 2. Creating and Using Virtual Threads
Basic Virtual Thread Creation
import java.time.Duration; import java.util.concurrent.Executors; import java.util.stream.IntStream; public class BasicVirtualThreads { public static void main(String[] args) throws Exception { demonstrateThreadCreation(); demonstrateMassiveConcurrency(); demonstrateExecutorService(); } public static void demonstrateThreadCreation() throws InterruptedException { System.out.println("=== Virtual Thread Creation ==="); // Method 1: Using Thread.ofVirtual() Thread virtualThread1 = Thread.ofVirtual() .name("virtual-thread-1") .start(() -> { System.out.println("Hello from virtual thread: " + Thread.currentThread()); try { Thread.sleep(1000); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } }); // Method 2: Using Thread.startVirtualThread() Thread virtualThread2 = Thread.startVirtualThread(() -> { System.out.println("Hello from virtual thread: " + Thread.currentThread()); try { Thread.sleep(1000); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } }); // Method 3: Using builder Thread.Builder virtualThreadBuilder = Thread.ofVirtual().name("worker-", 0); Thread virtualThread3 = virtualThreadBuilder.start(() -> { System.out.println("Hello from virtual thread: " + Thread.currentThread()); }); virtualThread1.join(); virtualThread2.join(); virtualThread3.join(); System.out.println("All virtual threads completed"); } public static void demonstrateMassiveConcurrency() throws InterruptedException { System.out.println("\n=== Massive Concurrency Demonstration ==="); int numberOfThreads = 100_000; System.out.println("Creating " + numberOfThreads + " virtual threads..."); long startTime = System.nanoTime(); var threads = IntStream.range(0, numberOfThreads) .mapToObj(i -> Thread.ofVirtual() .name("virtual-thread-", i) .start(() -> { try { // Simulate some work Thread.sleep(100); if (i % 10_000 == 0) { System.out.println("Thread " + i + " completed"); } } catch (InterruptedException e) { Thread.currentThread().interrupt(); } })) .toList(); // Wait for all threads to complete for (Thread thread : threads) { thread.join(); } long endTime = System.nanoTime(); long duration = (endTime - startTime) / 1_000_000; System.out.printf("Completed %d virtual threads in %d ms%n", numberOfThreads, duration); System.out.println("Memory used: " + (Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory()) / 1024 / 1024 + " MB"); } public static void demonstrateExecutorService() throws InterruptedException { System.out.println("\n=== Virtual Thread Executor Service ==="); try (var executor = Executors.newVirtualThreadPerTaskExecutor()) { // Submit multiple tasks var future1 = executor.submit(() -> { System.out.println("Task 1 running in: " + Thread.currentThread()); Thread.sleep(500); return "Result 1"; }); var future2 = executor.submit(() -> { System.out.println("Task 2 running in: " + Thread.currentThread()); Thread.sleep(300); return "Result 2"; }); var future3 = executor.submit(() -> { System.out.println("Task 3 running in: " + Thread.currentThread()); Thread.sleep(700); return "Result 3"; }); // Get results System.out.println("Future 1: " + future1.get()); System.out.println("Future 2: " + future2.get()); System.out.println("Future 3: " + future3.get()); } catch (Exception e) { e.printStackTrace(); } System.out.println("Executor service completed"); } public static void demonstrateThreadProperties() { System.out.println("\n=== Virtual Thread Properties ==="); Thread virtualThread = Thread.ofVirtual() .name("demo-virtual-thread") .start(() -> { Thread current = Thread.currentThread(); System.out.println("Thread name: " + current.getName()); System.out.println("Is virtual: " + current.isVirtual()); System.out.println("Thread ID: " + current.threadId()); System.out.println("Priority: " + current.getPriority()); System.out.println("Is daemon: " + current.isDaemon()); System.out.println("Thread group: " + current.getThreadGroup()); System.out.println("State: " + current.getState()); }); try { virtualThread.join(); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } } } 3. Real-World Use Cases
High-Concurrency Web Server
import java.io.IOException; import java.net.ServerSocket; import java.net.Socket; import java.util.concurrent.Executors; public class VirtualThreadWebServer { private static final int PORT = 8080; private static final int MAX_REQUESTS = 10_000; public static void main(String[] args) throws IOException { demonstrateWebServer(); } public static void demonstrateWebServer() throws IOException { System.out.println("=== Virtual Thread Web Server Demo ==="); try (var executor = Executors.newVirtualThreadPerTaskExecutor(); var serverSocket = new ServerSocket(PORT)) { System.out.println("Server listening on port " + PORT); System.out.println("Using virtual threads for request handling"); int requestCount = 0; while (requestCount < MAX_REQUESTS) { Socket clientSocket = serverSocket.accept(); requestCount++; executor.submit(() -> handleRequest(clientSocket, requestCount)); if (requestCount % 1000 == 0) { System.out.println("Processed " + requestCount + " requests"); } } } System.out.println("Server shutdown completed"); } private static void handleRequest(Socket clientSocket, int requestId) { try (clientSocket) { // Simulate request processing Thread.sleep(50); // I/O operation simulation // Simulate different types of requests String response = switch (requestId % 4) { case 0 -> processUserProfile(requestId); case 1 -> processOrder(requestId); case 2 -> processPayment(requestId); case 3 -> processSearch(requestId); default -> "Unknown request"; }; if (requestId % 500 == 0) { System.out.println("Request " + requestId + " processed: " + response); } } catch (IOException | InterruptedException e) { System.err.println("Error handling request " + requestId + ": " + e.getMessage()); Thread.currentThread().interrupt(); } } private static String processUserProfile(int requestId) throws InterruptedException { Thread.sleep(10); // Database query simulation return "User profile for request " + requestId; } private static String processOrder(int requestId) throws InterruptedException { Thread.sleep(20); // Order processing simulation return "Order processed for request " + requestId; } private static String processPayment(int requestId) throws InterruptedException { Thread.sleep(15); // Payment gateway simulation return "Payment completed for request " + requestId; } private static String processSearch(int requestId) throws InterruptedException { Thread.sleep(25); // Search index simulation return "Search results for request " + requestId; } } // Database connection pool simulation with virtual threads class DatabaseService { private static final int CONNECTION_POOL_SIZE = 20; public String queryUserData(int userId) throws InterruptedException { // Simulate database query Thread.sleep(10); return "User data for ID: " + userId; } public void processWithVirtualThreads() throws InterruptedException { try (var executor = Executors.newVirtualThreadPerTaskExecutor()) { // Process 1000 database queries concurrently for (int i = 1; i <= 1000; i++) { final int userId = i; executor.submit(() -> { try { String result = queryUserData(userId); if (userId % 100 == 0) { System.out.println("Processed user " + userId + ": " + result); } } catch (InterruptedException e) { Thread.currentThread().interrupt(); } }); } } } } I/O Intensive Operations
import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.util.ArrayList; import java.util.List; import java.util.concurrent.CompletableFuture; import java.util.concurrent.Executors; import java.util.concurrent.atomic.AtomicInteger; public class IOIntensiveOperations { public static void main(String[] args) throws Exception { demonstrateHttpRequests(); demonstrateFileOperations(); demonstrateDatabaseOperations(); } public static void demonstrateHttpRequests() throws InterruptedException { System.out.println("=== HTTP Requests with Virtual Threads ==="); int numberOfRequests = 100; AtomicInteger completedRequests = new AtomicInteger(); try (var executor = Executors.newVirtualThreadPerTaskExecutor(); var httpClient = HttpClient.newBuilder() .executor(executor) .build()) { List<CompletableFuture<Void>> futures = new ArrayList<>(); for (int i = 1; i <= numberOfRequests; i++) { final int requestId = i; CompletableFuture<Void> future = CompletableFuture.runAsync(() -> { try { // Simulate HTTP request to different endpoints String endpoint = switch (requestId % 4) { case 0 -> "https://httpbin.org/delay/1"; case 1 -> "https://httpbin.org/delay/2"; case 2 -> "https://httpbin.org/status/200"; case 3 -> "https://httpbin.org/bytes/1024"; default -> "https://httpbin.org/get"; }; HttpRequest request = HttpRequest.newBuilder() .uri(URI.create(endpoint)) .GET() .build(); // For real HTTP requests (commented to avoid actual network calls) // HttpResponse<String> response = httpClient.send(request, // HttpResponse.BodyHandlers.ofString()); // Simulate network delay instead Thread.sleep(100 + (requestId % 3) * 50); int count = completedRequests.incrementAndGet(); if (count % 10 == 0) { System.out.println("Completed " + count + " HTTP requests"); } } catch (Exception e) { System.err.println("Request " + requestId + " failed: " + e.getMessage()); } }, executor); futures.add(future); } // Wait for all requests to complete CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join(); } System.out.println("All " + completedRequests.get() + " HTTP requests completed"); } public static void demonstrateFileOperations() throws InterruptedException { System.out.println("\n=== File I/O with Virtual Threads ==="); int numberOfFiles = 500; AtomicInteger processedFiles = new AtomicInteger(); try (var executor = Executors.newVirtualThreadPerTaskExecutor()) { List<CompletableFuture<Void>> futures = new ArrayList<>(); for (int i = 1; i <= numberOfFiles; i++) { final int fileId = i; CompletableFuture<Void> future = CompletableFuture.runAsync(() -> { try { // Simulate file operations simulateFileRead(fileId); simulateFileProcess(fileId); simulateFileWrite(fileId); int count = processedFiles.incrementAndGet(); if (count % 50 == 0) { System.out.println("Processed " + count + " files"); } } catch (InterruptedException e) { Thread.currentThread().interrupt(); } }, executor); futures.add(future); } CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join(); } System.out.println("All " + processedFiles.get() + " file operations completed"); } private static void simulateFileRead(int fileId) throws InterruptedException { Thread.sleep(20); // Simulate disk I/O } private static void simulateFileProcess(int fileId) throws InterruptedException { Thread.sleep(10); // Simulate CPU work } private static void simulateFileWrite(int fileId) throws InterruptedException { Thread.sleep(15); // Simulate disk I/O } public static void demonstrateDatabaseOperations() { System.out.println("\n=== Database Operations with Virtual Threads ==="); DatabaseConnectionPool pool = new DatabaseConnectionPool(10); int numberOfQueries = 1000; long startTime = System.nanoTime(); try (var executor = Executors.newVirtualThreadPerTaskExecutor()) { List<CompletableFuture<String>> futures = new ArrayList<>(); for (int i = 1; i <= numberOfQueries; i++) { final int queryId = i; CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> { try { return pool.executeQuery("SELECT * FROM data WHERE id = " + queryId); } catch (InterruptedException e) { Thread.currentThread().interrupt(); return "Query " + queryId + " interrupted"; } }, executor); futures.add(future); } // Process results as they complete AtomicInteger completed = new AtomicInteger(); futures.forEach(future -> { future.thenAccept(result -> { int count = completed.incrementAndGet(); if (count % 100 == 0) { System.out.println("Completed " + count + " database queries"); } }); }); CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join(); } long endTime = System.nanoTime(); long duration = (endTime - startTime) / 1_000_000; System.out.printf("Completed %d database queries in %d ms%n", numberOfQueries, duration); } } class DatabaseConnectionPool { private final int poolSize; private final AtomicInteger availableConnections; public DatabaseConnectionPool(int poolSize) { this.poolSize = poolSize; this.availableConnections = new AtomicInteger(poolSize); } public String executeQuery(String query) throws InterruptedException { // Simulate getting connection from pool while (availableConnections.get() <= 0) { Thread.sleep(1); // Wait for connection } availableConnections.decrementAndGet(); try { // Simulate database query execution Thread.sleep(5); // Network + database processing return "Result for: " + query; } finally { availableConnections.incrementAndGet(); } } } 4. Advanced Virtual Thread Features
Pinned Virtual Threads and Monitoring
import java.lang.management.ManagementFactory; import java.lang.management.ThreadMXBean; import java.util.concurrent.Executors; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; public class AdvancedVirtualThreads { public static void main(String[] args) throws Exception { demonstratePinnedThreads(); demonstrateThreadLocalUsage(); demonstrateMonitoring(); demonstrateStructuredConcurrency(); } public static void demonstratePinnedThreads() throws InterruptedException { System.out.println("=== Pinned Virtual Threads ==="); // Virtual threads can get "pinned" to carrier threads in certain situations Object monitor = new Object(); try (var executor = Executors.newVirtualThreadPerTaskExecutor()) { for (int i = 0; i < 5; i++) { final int taskId = i; executor.submit(() -> { System.out.println("Task " + taskId + " started in: " + Thread.currentThread()); // Synchronized block - can cause pinning synchronized (monitor) { try { System.out.println("Task " + taskId + " in synchronized block, thread: " + Thread.currentThread()); Thread.sleep(100); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } } System.out.println("Task " + taskId + " completed"); }); } } System.out.println("\nUsing ReentrantLock instead of synchronized:"); Lock lock = new ReentrantLock(); try (var executor = Executors.newVirtualThreadPerTaskExecutor()) { for (int i = 0; i < 5; i++) { final int taskId = i; executor.submit(() -> { System.out.println("Task " + taskId + " started in: " + Thread.currentThread()); // Using Lock instead of synchronized - no pinning lock.lock(); try { System.out.println("Task " + taskId + " in locked section, thread: " + Thread.currentThread()); Thread.sleep(100); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } finally { lock.unlock(); } System.out.println("Task " + taskId + " completed"); }); } } } public static void demonstrateThreadLocalUsage() throws InterruptedException { System.out.println("\n=== ThreadLocal with Virtual Threads ==="); ThreadLocal<String> userContext = new ThreadLocal<>(); InheritableThreadLocal<String> inheritableContext = new InheritableThreadLocal<>(); try (var executor = Executors.newVirtualThreadPerTaskExecutor()) { for (int i = 0; i < 5; i++) { final int userId = i; executor.submit(() -> { // Set thread-local values userContext.set("User-" + userId); inheritableContext.set("Inheritable-User-" + userId); System.out.println("ThreadLocal: " + userContext.get()); System.out.println("InheritableThreadLocal: " + inheritableContext.get()); // Simulate work try { Thread.sleep(50); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } // Clean up (important for virtual threads!) userContext.remove(); inheritableContext.remove(); }); } } System.out.println("Note: ThreadLocal values should be cleaned up to prevent memory leaks"); } public static void demonstrateMonitoring() throws InterruptedException { System.out.println("\n=== Virtual Thread Monitoring ==="); ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean(); System.out.println("Current thread count: " + threadMXBean.getThreadCount()); System.out.println("Peak thread count: " + threadMXBean.getPeakThreadCount()); // Create some virtual threads and monitor try (var executor = Executors.newVirtualThreadPerTaskExecutor()) { for (int i = 0; i < 100; i++) { executor.submit(() -> { try { Thread.sleep(100); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } }); } // Give threads time to start Thread.sleep(50); System.out.println("Thread count after creating virtual threads: " + threadMXBean.getThreadCount()); } System.out.println("Thread count after executor closed: " + threadMXBean.getThreadCount()); } public static void demonstrateStructuredConcurrency() throws InterruptedException { System.out.println("\n=== Structured Concurrency with Virtual Threads ==="); // Using ExecutorService automatically provides structured concurrency try (var executor = Executors.newVirtualThreadPerTaskExecutor()) { var userFuture = executor.submit(() -> { System.out.println("Fetching user data..."); Thread.sleep(100); return "User Data"; }); var orderFuture = executor.submit(() -> { System.out.println("Fetching order history..."); Thread.sleep(150); return "Order History"; }); var paymentFuture = executor.submit(() -> { System.out.println("Fetching payment info..."); Thread.sleep(80); return "Payment Info"; }); try { String userData = userFuture.get(); String orderHistory = orderFuture.get(); String paymentInfo = paymentFuture.get(); System.out.println("Combined result:"); System.out.println(" - " + userData); System.out.println(" - " + orderHistory); System.out.println(" - " + paymentInfo); } catch (Exception e) { System.err.println("Error fetching data: " + e.getMessage()); // All tasks are automatically cancelled if any fail } } System.out.println("All tasks completed within structured scope"); } } 5. Performance Comparison
Virtual Threads vs Platform Threads Benchmark
import java.util.concurrent.*; import java.util.concurrent.atomic.AtomicInteger; import java.util.stream.IntStream; public class VirtualThreadsBenchmark { private static final int TASK_COUNT = 10_000; private static final int IO_DELAY_MS = 100; public static void main(String[] args) throws Exception { runPlatformThreadsBenchmark(); runVirtualThreadsBenchmark(); runMixedWorkloadBenchmark(); } public static void runPlatformThreadsBenchmark() throws InterruptedException { System.out.println("=== Platform Threads Benchmark ==="); long startTime = System.nanoTime(); long memoryBefore = getUsedMemory(); try (var executor = Executors.newFixedThreadPool(200)) { // Limited to 200 threads due to platform thread constraints AtomicInteger completed = new AtomicInteger(); CountDownLatch latch = new CountDownLatch(TASK_COUNT); for (int i = 0; i < TASK_COUNT; i++) { final int taskId = i; executor.submit(() -> { try { // Simulate I/O operation Thread.sleep(IO_DELAY_MS); int count = completed.incrementAndGet(); if (count % 1000 == 0) { System.out.println("Platform threads completed: " + count); } } catch (InterruptedException e) { Thread.currentThread().interrupt(); } finally { latch.countDown(); } }); } latch.await(); } long endTime = System.nanoTime(); long duration = (endTime - startTime) / 1_000_000; long memoryUsed = getUsedMemory() - memoryBefore; System.out.printf("Platform threads: %d tasks in %d ms, memory: %d KB%n", TASK_COUNT, duration, memoryUsed / 1024); } public static void runVirtualThreadsBenchmark() throws InterruptedException { System.out.println("\n=== Virtual Threads Benchmark ==="); long startTime = System.nanoTime(); long memoryBefore = getUsedMemory(); try (var executor = Executors.newVirtualThreadPerTaskExecutor()) { // Can handle all 10,000 tasks concurrently AtomicInteger completed = new AtomicInteger(); CountDownLatch latch = new CountDownLatch(TASK_COUNT); for (int i = 0; i < TASK_COUNT; i++) { final int taskId = i; executor.submit(() -> { try { // Simulate I/O operation Thread.sleep(IO_DELAY_MS); int count = completed.incrementAndGet(); if (count % 1000 == 0) { System.out.println("Virtual threads completed: " + count); } } catch (InterruptedException e) { Thread.currentThread().interrupt(); } finally { latch.countDown(); } }); } latch.await(); } long endTime = System.nanoTime(); long duration = (endTime - startTime) / 1_000_000; long memoryUsed = getUsedMemory() - memoryBefore; System.out.printf("Virtual threads: %d tasks in %d ms, memory: %d KB%n", TASK_COUNT, duration, memoryUsed / 1024); } public static void runMixedWorkloadBenchmark() throws InterruptedException { System.out.println("\n=== Mixed Workload Benchmark ==="); int[] workloads = {100, 1000, 5000, 10000}; for (int workload : workloads) { System.out.println("\nWorkload: " + workload + " tasks"); // Platform threads long platformTime = benchmarkPlatformThreads(workload); long virtualTime = benchmarkVirtualThreads(workload); System.out.printf("Platform: %d ms, Virtual: %d ms, Speedup: %.2fx%n", platformTime, virtualTime, (double) platformTime / virtualTime); } } private static long benchmarkPlatformThreads(int taskCount) throws InterruptedException { int threadPoolSize = Math.min(taskCount, 200); long startTime = System.nanoTime(); try (var executor = Executors.newFixedThreadPool(threadPoolSize)) { CountDownLatch latch = new CountDownLatch(taskCount); for (int i = 0; i < taskCount; i++) { executor.submit(() -> { try { Thread.sleep(IO_DELAY_MS); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } finally { latch.countDown(); } }); } latch.await(); } return (System.nanoTime() - startTime) / 1_000_000; } private static long benchmarkVirtualThreads(int taskCount) throws InterruptedException { long startTime = System.nanoTime(); try (var executor = Executors.newVirtualThreadPerTaskExecutor()) { CountDownLatch latch = new CountDownLatch(taskCount); for (int i = 0; i < taskCount; i++) { executor.submit(() -> { try { Thread.sleep(IO_DELAY_MS); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } finally { latch.countDown(); } }); } latch.await(); } return (System.nanoTime() - startTime) / 1_000_000; } private static long getUsedMemory() { Runtime runtime = Runtime.getRuntime(); return runtime.totalMemory() - runtime.freeMemory(); } public static void demonstrateCPUvsIO() throws InterruptedException { System.out.println("\n=== CPU-bound vs I/O-bound Workloads ==="); System.out.println(""" Virtual Threads Performance Characteristics: I/O-bound workloads: - Excellent performance - Massive concurrency - Minimal memory overhead - Perfect for web servers, databases, network calls CPU-bound workloads: - Similar performance to platform threads - No significant advantage - Still useful for mixed workloads Best used for: - HTTP servers/clients - Database operations - File I/O - Message queues - Any blocking I/O operations """); } } 6. Best Practices and Migration
Virtual Threads Best Practices
import java.util.concurrent.*; public class VirtualThreadsBestPractices { public static void main(String[] args) { demonstrateBestPractices(); demonstrateMigrationStrategies(); demonstrateAntiPatterns(); } public static void demonstrateBestPractices() { System.out.println("=== Virtual Threads Best Practices ==="); System.out.println(""" ✅ DO: 1. Use for I/O-bound workloads - HTTP requests, database calls, file operations 2. Use Executors.newVirtualThreadPerTaskExecutor() - Automatic resource management - Structured concurrency 3. Clean up ThreadLocal variables - Call remove() to prevent memory leaks 4. Use locks instead of synchronized - Avoids carrier thread pinning 5. Monitor thread creation - Use JVM metrics for monitoring 6. Handle exceptions properly - Virtual threads behave like platform threads Configuration: - Use default settings for most cases - Adjust -XX:VirtualThreadSchedulerParallelism for CPU-bound mix - Monitor with JDK flight recorder """); } public static void demonstrateMigrationStrategies() throws InterruptedException { System.out.println("\n=== Migration Strategies ==="); // Before migration - platform threads ExecutorService oldExecutor = Executors.newFixedThreadPool(100); // After migration - virtual threads ExecutorService newExecutor = Executors.newVirtualThreadPerTaskExecutor(); System.out.println("Migration example:"); System.out.println(" Before: Executors.newFixedThreadPool(100)"); System.out.println(" After: Executors.newVirtualThreadPerTaskExecutor()"); // Same task submission code works for both Runnable task = () -> { try { Thread.sleep(100); System.out.println("Task executed in: " + Thread.currentThread()); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } }; oldExecutor.submit(task); newExecutor.submit(task); oldExecutor.shutdown(); newExecutor.shutdown(); oldExecutor.awaitTermination(1, TimeUnit.SECONDS); newExecutor.awaitTermination(1, TimeUnit.SECONDS); System.out.println("Same code works with both executors!"); } public static void demonstrateAntiPatterns() { System.out.println("\n=== Virtual Threads Anti-Patterns ==="); System.out.println(""" ❌ DON'T: 1. Use for CPU-bound workloads only - No benefit over platform threads - May even have slight overhead 2. Create unlimited virtual threads for CPU work - Can still overwhelm the system - Use bounded executors for CPU work 3. Forget to clean up resources - ThreadLocal, socket connections, file handles 4. Use synchronized with long operations - Causes carrier thread pinning - Use ReentrantLock instead 5. Assume all blocking is magically cheap - Underlying I/O operations still have costs - Monitor actual resource usage 6. Ignore exception handling - Virtual threads propagate exceptions like platform threads """); demonstrateCommonMistakes(); } public static void demonstrateCommonMistakes() { System.out.println("\n=== Common Mistakes ==="); // Mistake 1: Not cleaning up ThreadLocal ThreadLocal<String> context = new ThreadLocal<>(); try (var executor = Executors.newVirtualThreadPerTaskExecutor()) { executor.submit(() -> { context.set("some-value"); // Forgot to call context.remove() - memory leak! }); } // Solution: Always clean up try (var executor = Executors.newVirtualThreadPerTaskExecutor()) { executor.submit(() -> { try { context.set("some-value"); // Do work... } finally { context.remove(); // Proper cleanup } }); } // Mistake 2: Using synchronized with long operations Object lock = new Object(); try (var executor = Executors.newVirtualThreadPerTaskExecutor()) { executor.submit(() -> { synchronized (lock) { // This pins the carrier thread! try { Thread.sleep(1000); // Long operation } catch (InterruptedException e) { Thread.currentThread().interrupt(); } } }); } // Solution: Use Lock instead Lock properLock = new ReentrantLock(); try (var executor = Executors.newVirtualThreadPerTaskExecutor()) { executor.submit(() -> { properLock.lock(); try { Thread.sleep(1000); // No pinning! } catch (InterruptedException e) { Thread.currentThread().interrupt(); } finally { properLock.unlock(); } }); } System.out.println("Avoid these common pitfalls for optimal performance"); } public static void showConfigurationOptions() { System.out.println("\n=== JVM Configuration Options ==="); System.out.println(""" Virtual Threads JVM Options: -XX:+UseVirtualThreads Enable virtual threads (default in future versions) -XX:VirtualThreadSchedulerParallelism=<number> Number of carrier threads (defaults to CPU cores) -XX:VirtualThreadStackSize=<size> Stack size for virtual threads -Xlog:vthread*=debug Enable virtual thread logging Monitoring: - Use JFR (Java Flight Recorder) for detailed metrics - JMX beans for thread count and status - OS monitoring for overall system resources """); } } Conclusion
Virtual Threads Benefits Summary:
| Aspect | Platform Threads | Virtual Threads |
|---|---|---|
| Creation Cost | ~1MB | ~200 bytes |
| Maximum Count | 1000-10000 | Millions |
| Blocking Impact | Blocks OS thread | Doesn't block carrier |
| Use Case | CPU-bound | I/O-bound |
| Programming Model | Same | Same |
When to Use Virtual Threads:
| Scenario | Recommendation |
|---|---|
| Web servers | ✅ Perfect fit |
| Database clients | ✅ Excellent |
| HTTP clients | ✅ Ideal |
| File processing | ✅ Great |
| CPU-intensive work | ⚠️ No significant benefit |
| Mixed workloads | ✅ Good choice |
Migration Strategy:
- Replace thread pools with
Executors.newVirtualThreadPerTaskExecutor() - Keep existing code - same APIs work
- Monitor performance and resource usage
- Clean up ThreadLocal variables
- Prefer Lock over synchronized for long operations
Java Version Requirements:
- Java 19: Initial preview
- Java 20: Second preview
- Java 21: Production ready
- Java 22+: Enhanced and optimized
Virtual threads revolutionize Java concurrency by enabling massive scale for I/O-bound applications while maintaining the simple thread-per-request programming model that developers love.