Fiber-Based Lightweight Threads in Java: Complete Virtual Threads Guide

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:

AspectPlatform ThreadsVirtual Threads
Creation Cost~1MB~200 bytes
Maximum Count1000-10000Millions
Blocking ImpactBlocks OS threadDoesn't block carrier
Use CaseCPU-boundI/O-bound
Programming ModelSameSame

When to Use Virtual Threads:

ScenarioRecommendation
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:

  1. Replace thread pools with Executors.newVirtualThreadPerTaskExecutor()
  2. Keep existing code - same APIs work
  3. Monitor performance and resource usage
  4. Clean up ThreadLocal variables
  5. 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.

Leave a Reply

Your email address will not be published. Required fields are marked *


Macro Nepal Helper