8.1 Virtual Thread Fundamentals
Virtual threads, introduced as a preview in Java 19 and finalized in Java 21, represent a revolutionary change in Java's concurrency model. They enable highly scalable concurrent applications with simple, synchronous code.
What Are Virtual Threads?
Traditional Platform Threads:
- Heavy-weight, one-to-one mapping with OS threads
- Each thread consumes ~2MB of memory (default stack size)
- Thread creation and context switching are expensive
- Limited scalability (typically thousands of threads max)
Virtual Threads:
- Light-weight, managed by JVM (not OS)
- Minimal memory footprint (~few KB per thread)
- Millions of virtual threads possible
- Scheduled on a pool of carrier (platform) threads
- Blocking operations don't block carrier threads
// Platform thread - traditional
Thread platformThread = new Thread(() -> {
System.out.println("Platform thread: " + Thread.currentThread());
});
platformThread.start();
// Virtual thread - modern
Thread virtualThread = Thread.startVirtualThread(() -> {
System.out.println("Virtual thread: " + Thread.currentThread());
});
Why Virtual Threads?
The Problem with Platform Threads:
// Traditional approach - limited scalability
ExecutorService executor = Executors.newFixedThreadPool(200);
// Can only handle 200 concurrent requests
for (int i = 0; i < 10_000; i++) {
int taskId = i;
executor.submit(() -> {
// If this blocks for I/O, thread is wasted
String data = fetchFromDatabase(taskId);
processData(data);
});
}
// Many tasks queued, waiting for available threads
Virtual Thread Solution:
// Virtual thread approach - scales to millions
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
for (int i = 0; i < 10_000; i++) {
int taskId = i;
executor.submit(() -> {
// Blocking is fine - virtual thread parks efficiently
String data = fetchFromDatabase(taskId);
processData(data);
});
}
} // All 10,000 tasks run concurrently!
Creating Virtual Threads
Method 1: Thread.startVirtualThread()
// Start and run immediately
Thread vt = Thread.startVirtualThread(() -> {
System.out.println("Running in: " + Thread.currentThread());
// do work
});
// Wait for completion
vt.join();
Method 2: Thread.ofVirtual()
// Build but don't start
Thread vt = Thread.ofVirtual()
.name("worker-1")
.unstarted(() -> {
System.out.println("Virtual thread: " + Thread.currentThread().getName());
// do work
});
// Start later
vt.start();
vt.join();
Method 3: Virtual Thread per Task Executor
// Most common pattern for concurrent tasks
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
// Submit multiple tasks
Future<String> future1 = executor.submit(() -> fetchData(1));
Future<String> future2 = executor.submit(() -> fetchData(2));
Future<String> future3 = executor.submit(() -> fetchData(3));
// Get results
System.out.println(future1.get());
System.out.println(future2.get());
System.out.println(future3.get());
}
Method 4: ThreadFactory
// Custom thread factory for frameworks
ThreadFactory factory = Thread.ofVirtual().factory();
Thread vt1 = factory.newThread(() -> task1());
Thread vt2 = factory.newThread(() -> task2());
vt1.start();
vt2.start();
How Virtual Threads Work
Carrier Threads:
┌─────────────────────────────────────────────┐
│ ForkJoinPool (Carrier Threads) │
│ Platform Thread 1 │ Platform Thread 2 │
└─────────────────────────────────────────────┘
↑ ↑
│ scheduled on │ scheduled on
│ │
┌──────────┴───────┐ ┌────────┴──────────┐
│ Virtual Thread 1 │ │ Virtual Thread 2 │
│ Virtual Thread 3 │ │ Virtual Thread 4 │
│ Virtual Thread 5 │ │ Virtual Thread 6 │
└──────────────────┘ └───────────────────┘
(parked when (parked when
blocking) blocking)
Execution Model:
- Virtual threads are scheduled on carrier (platform) threads
- When a virtual thread blocks (I/O, sleep), it's unmounted from carrier
- Carrier thread picks up another virtual thread
- When I/O completes, virtual thread is remounted on any available carrier
// Demonstration of parking behavior
Thread.startVirtualThread(() -> {
System.out.println("VT-1 Start on: " + Thread.currentThread());
try {
Thread.sleep(100); // VT-1 parks, carrier freed
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
// VT-1 resumes on potentially different carrier
System.out.println("VT-1 Resume on: " + Thread.currentThread());
});
Comparison: Platform vs Virtual Threads
| Aspect | Platform Threads | Virtual Threads |
|---|---|---|
| Memory per thread | ~2 MB | ~few KB |
| Max concurrent threads | ~thousands | ~millions |
| Creation cost | High | Very low |
| Context switch | OS-level | JVM-level |
| Blocking behavior | Blocks OS thread | Parks (unmounts) |
| Thread pool needed | Yes | No |
| Best for | CPU-bound tasks | I/O-bound tasks |
Basic Examples
Example 1: Simple HTTP Requests
import java.net.URI;
import java.net.http.*;
import java.time.Duration;
import java.util.*;
public class BasicVirtualThreadExample {
public static void main(String[] args) throws InterruptedException {
List<String> urls = List.of(
"https://api.github.com/users/github",
"https://api.github.com/users/microsoft",
"https://api.github.com/users/google"
);
// With virtual threads - all requests concurrent
long start = System.currentTimeMillis();
List<Thread> threads = new ArrayList<>();
for (String url : urls) {
Thread vt = Thread.startVirtualThread(() -> {
try {
HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(url))
.timeout(Duration.ofSeconds(5))
.build();
HttpResponse<String> response = client.send(
request,
HttpResponse.BodyHandlers.ofString()
);
System.out.printf("%s: %d bytes%n",
url, response.body().length());
} catch (Exception e) {
System.err.println("Failed: " + url + " - " + e.getMessage());
}
});
threads.add(vt);
}
// Wait for all to complete
for (Thread vt : threads) {
vt.join();
}
long duration = System.currentTimeMillis() - start;
System.out.println("Completed in: " + duration + "ms");
}
}
Example 2: Database Queries
import java.sql.*;
import java.util.*;
import java.util.concurrent.*;
public class DatabaseQueryExample {
public static void main(String[] args) throws Exception {
List<Integer> userIds = List.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
List<Future<User>> futures = userIds.stream()
.map(id -> executor.submit(() -> fetchUser(id)))
.toList();
// Collect results
List<User> users = futures.stream()
.map(f -> {
try {
return f.get();
} catch (Exception e) {
throw new RuntimeException(e);
}
})
.toList();
System.out.println("Fetched " + users.size() + " users");
}
}
private static User fetchUser(int id) {
try (Connection conn = getConnection()) {
PreparedStatement stmt = conn.prepareStatement(
"SELECT * FROM users WHERE id = ?"
);
stmt.setInt(1, id);
ResultSet rs = stmt.executeQuery();
if (rs.next()) {
return new User(
rs.getInt("id"),
rs.getString("name"),
rs.getString("email")
);
}
return null;
} catch (SQLException e) {
throw new RuntimeException("Failed to fetch user: " + id, e);
}
}
private static Connection getConnection() throws SQLException {
return DriverManager.getConnection(
"jdbc:postgresql://localhost/mydb",
"user",
"password"
);
}
record User(int id, String name, String email) {}
}
Example 3: File Processing
import java.io.IOException;
import java.nio.file.*;
import java.util.*;
import java.util.concurrent.*;
public class FileProcessingExample {
public static void main(String[] args) throws Exception {
Path inputDir = Path.of("input");
Path outputDir = Path.of("output");
Files.createDirectories(outputDir);
List<Path> files = Files.list(inputDir)
.filter(Files::isRegularFile)
.toList();
System.out.println("Processing " + files.size() + " files...");
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
List<Future<Void>> futures = files.stream()
.map(file -> executor.submit(() -> {
processFile(file, outputDir);
return null;
}))
.toList();
// Wait for all
for (Future<Void> future : futures) {
future.get();
}
}
System.out.println("All files processed!");
}
private static void processFile(Path input, Path outputDir) {
try {
String content = Files.readString(input);
// Simulate processing
String processed = content.toUpperCase();
Path output = outputDir.resolve(input.getFileName());
Files.writeString(output, processed);
System.out.println("Processed: " + input.getFileName());
} catch (IOException e) {
System.err.println("Error processing " + input + ": " + e.getMessage());
}
}
}
Thread Properties
Checking if Thread is Virtual:
Thread thread = Thread.currentThread();
if (thread.isVirtual()) {
System.out.println("Running in virtual thread");
} else {
System.out.println("Running in platform thread");
}
Thread Names:
// Auto-generated names
Thread vt1 = Thread.startVirtualThread(() -> {
System.out.println(Thread.currentThread().getName());
// Output: VirtualThread-0 (or similar)
});
// Custom names
Thread vt2 = Thread.ofVirtual()
.name("data-processor-1")
.start(() -> {
System.out.println(Thread.currentThread().getName());
// Output: data-processor-1
});
// Name prefix for executor
ThreadFactory factory = Thread.ofVirtual()
.name("worker-", 0)
.factory();
ExecutorService executor = Executors.newThreadPerTaskExecutor(factory);
Thread Priority (No Effect):
// Virtual threads ignore priority
Thread vt = Thread.ofVirtual()
.priority(Thread.MAX_PRIORITY) // Ignored!
.start(() -> {
// priority has no effect on scheduling
});
Thread Groups (Deprecated):
// Virtual threads don't support thread groups
Thread vt = Thread.startVirtualThread(() -> {
ThreadGroup group = Thread.currentThread().getThreadGroup();
System.out.println(group); // null
});
When to Use Virtual Threads
✅ Good Use Cases:
- High-throughput servers (web servers, API gateways)
- I/O-bound workloads (database queries, file operations)
- Network operations (HTTP clients, message queues)
- Microservices with many blocking calls
- Request-per-thread model applications
// Perfect for web server handling many concurrent requests
public class SimpleWebServer {
public void handleRequests() throws IOException {
try (var server = ServerSocketChannel.open()) {
server.bind(new InetSocketAddress(8080));
while (true) {
SocketChannel client = server.accept();
// Spawn virtual thread per request
Thread.startVirtualThread(() -> handleClient(client));
}
}
}
private void handleClient(SocketChannel client) {
try {
// Blocking I/O is fine - virtual thread parks
ByteBuffer buffer = ByteBuffer.allocate(1024);
client.read(buffer);
// Process request (may call database, other services)
String response = processRequest(buffer);
// Write response
client.write(ByteBuffer.wrap(response.getBytes()));
client.close();
} catch (IOException e) {
System.err.println("Error handling client: " + e.getMessage());
}
}
}
❌ Poor Use Cases:
- CPU-bound tasks (use ForkJoinPool or parallel streams)
- Very short-lived tasks (overhead not worth it)
- Tasks requiring thread affinity
- Code with heavy synchronized block usage
// Bad: CPU-intensive task
// Use ForkJoinPool or parallel streams instead
IntStream.range(0, 1_000_000)
.parallel() // Better than virtual threads for CPU work
.map(i -> expensiveComputation(i))
.sum();
// Bad: Very short tasks
for (int i = 0; i < 1_000_000; i++) {
Thread.startVirtualThread(() -> {
// Just incrementing a counter - overhead exceeds benefit
counter.incrementAndGet();
});
}
Performance Characteristics
Memory Usage:
// Platform threads
ExecutorService platform = Executors.newFixedThreadPool(1000);
// Memory: ~2 GB (2 MB × 1000 threads)
// Virtual threads
ExecutorService virtual = Executors.newVirtualThreadPerTaskExecutor();
// Memory: ~few MB for thousands of virtual threads
Throughput Comparison:
import java.time.Duration;
import java.time.Instant;
import java.util.*;
import java.util.concurrent.*;
public class ThroughputComparison {
public static void main(String[] args) throws Exception {
int numTasks = 10_000;
// Platform threads - limited by pool size
System.out.println("Platform threads:");
benchmarkPlatform(numTasks);
// Virtual threads - scales to all tasks
System.out.println("\nVirtual threads:");
benchmarkVirtual(numTasks);
}
private static void benchmarkPlatform(int numTasks) throws Exception {
Instant start = Instant.now();
try (var executor = Executors.newFixedThreadPool(200)) {
List<Future<?>> futures = new ArrayList<>();
for (int i = 0; i < numTasks; i++) {
futures.add(executor.submit(() -> {
simulateIO();
}));
}
for (Future<?> future : futures) {
future.get();
}
}
Duration duration = Duration.between(start, Instant.now());
System.out.println("Time: " + duration.toMillis() + "ms");
}
private static void benchmarkVirtual(int numTasks) throws Exception {
Instant start = Instant.now();
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
List<Future<?>> futures = new ArrayList<>();
for (int i = 0; i < numTasks; i++) {
futures.add(executor.submit(() -> {
simulateIO();
}));
}
for (Future<?> future : futures) {
future.get();
}
}
Duration duration = Duration.between(start, Instant.now());
System.out.println("Time: " + duration.toMillis() + "ms");
}
private static void simulateIO() {
try {
Thread.sleep(100); // Simulate blocking I/O
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
// Typical results:
// Platform threads (200 pool): ~5000ms (200 threads × 50 batches × 100ms)
// Virtual threads: ~100ms (all 10,000 tasks run concurrently)
Best Practices
Use Virtual-Thread-Per-Task Executor
// Good try (var executor = Executors.newVirtualThreadPerTaskExecutor()) { executor.submit(() -> task()); } // Avoid creating threads manually in loops for (int i = 0; i < 10000; i++) { Thread.startVirtualThread(() -> task()); // Less structured }Let Virtual Threads Block
// Good - simple blocking code String data = blockingHttpCall(); processData(data); // Don't use async patterns // completableFuture.thenApply(...).thenApply(...) // Unnecessary complexityName Your Threads
Thread.ofVirtual() .name("order-processor-" + orderId) .start(() -> processOrder(orderId));Use Try-With-Resources
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) { // submit tasks } // Automatically shuts down and waitsDon't Pool Virtual Threads
// Bad - defeats the purpose ExecutorService pool = Executors.newFixedThreadPool( 100, Thread.ofVirtual().factory() ); // Good - one virtual thread per task var executor = Executors.newVirtualThreadPerTaskExecutor();