Java 19: Virtual Threads and Structured Concurrency

Java 19: Virtual Threads and Structured Concurrency
Java 19 (September 2022) introduced Virtual Threads (preview), a revolutionary approach to concurrent programming inspired by Project Loom. This is one of the most important additions to modern Java.
1. Virtual Threads (Preview)
Lightweight threads that make concurrent programming much simpler.
Before Virtual Threads (Traditional Threads):
// Traditional threading - expensive (heavy-weight threads)
public class TraditionalThreads {
public static void main(String[] args) throws InterruptedException {
int NUM_THREADS = 1000;
Thread[] threads = new Thread[NUM_THREADS];
// Creating 1000 threads consumes significant memory
// Each thread = ~1MB stack memory
for (int i = 0; i < NUM_THREADS; i++) {
final int id = i;
threads[i] = new Thread(() -> {
try {
// Simulate blocking I/O
Thread.sleep(1000);
System.out.println("Thread " + id + " completed");
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
threads[i].start();
}
for (Thread t : threads) {
t.join();
}
// Memory usage: ~1GB for 1000 threads!
// Context switching overhead is significant
}
}With Virtual Threads (Java 19+):
// Virtual threads - lightweight (can create millions!)
import java.util.concurrent.*;
public class VirtualThreads {
public static void main(String[] args) throws InterruptedException {
int NUM_VIRTUAL_THREADS = 100_000;
// Create 100,000 virtual threads!
try (ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor()) {
for (int i = 0; i < NUM_VIRTUAL_THREADS; i++) {
final int id = i;
executor.submit(() -> {
try {
// Same blocking I/O code, but much cheaper!
Thread.sleep(1000);
System.out.println("Virtual thread " + id);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
}
}
// Memory usage: ~10MB for 100,000 virtual threads!
// Context switching is handled by JVM efficiently
}
}Performance Impact:
Traditional Threads:
- 1,000 threads ≈ 1GB memory
- High context switching overhead
- Limited scalability
Virtual Threads:
- 1,000,000 threads ≈ 10MB memory
- Minimal context switching
- Extreme scalability2. Creating Virtual Threads
Multiple ways to create and manage virtual threads.
// Method 1: newVirtualThreadPerTaskExecutor
ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor();
executor.submit(() -> System.out.println("Running on virtual thread"));
// Method 2: Direct creation
Thread vthread = Thread.ofVirtual()
.name("VT-", 1)
.start(() -> System.out.println("Custom virtual thread"));
// Method 3: Virtual thread builder
Thread virtualThread = Thread.ofVirtual()
.name("worker-1")
.unstarted(() -> {
System.out.println("Work on virtual thread");
});
virtualThread.start();
// Method 4: Virtual thread factory
ThreadFactory virtualThreadFactory = Thread.ofVirtual()
.name("vthread-", 0)
.factory();
Thread vt = virtualThreadFactory.newThread(() -> {
// Virtual thread code
});
vt.start();3. Structured Concurrency (Preview)
Better coordination and error handling for concurrent tasks.
// Structured Concurrency - manages related tasks together
import java.util.concurrent.StructuredTaskScope;
public class StructuredConcurrencyExample {
// Traditional approach - error-prone
public static void traditionalApproach() throws Exception {
Future<String> userFuture = executor.submit(() -> fetchUser());
Future<String> permissionsFuture = executor.submit(() -> fetchPermissions());
String user = userFuture.get(5, TimeUnit.SECONDS);
String permissions = permissionsFuture.get(5, TimeUnit.SECONDS);
// Manual cleanup and timeout handling - verbose
}
// Structured Concurrency - cleaner
public static void structuredApproach() throws Exception {
try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
Future<String> userFuture = scope.fork(task("user"));
Future<String> permsFuture = scope.fork(task("permissions"));
Future<String> roleFuture = scope.fork(task("roles"));
scope.join(); // Wait for all to complete
String user = userFuture.resultNow();
String perms = permsFuture.resultNow();
String role = roleFuture.resultNow();
// Automatic cleanup if any task fails
// All tasks are properly coordinated
}
}
private static Callable<String> task(String name) {
return () -> {
Thread.sleep(100); // Simulate work
return name + " result";
};
}
}4. Real-World Virtual Threads Example
Web services become dramatically more scalable.
// Web server handling thousands of concurrent requests
import com.sun.net.httpserver.*;
import java.util.concurrent.*;
public class HighConcurrencyServer {
public static void main(String[] args) throws IOException {
HttpServer server = HttpServer.create(new InetSocketAddress(8080), 0);
// Use virtual threads for each request
ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor();
server.createContext("/api/user", exchange -> {
// Handle request on a virtual thread
String userId = extractUserId(exchange);
String userData = fetchUserData(userId); // Blocking I/O
String userPermissions = fetchPermissions(userId); // Blocking I/O
String response = userData + "," + userPermissions;
exchange.getResponseHeaders().set("Content-Type", "application/json");
exchange.sendResponseHeaders(200, response.getBytes().length);
exchange.getResponseBody().write(response.getBytes());
exchange.close();
});
server.setExecutor(executor);
server.start();
System.out.println("Server handling millions of concurrent connections!");
}
// Blocking I/O operations now become viable at scale
static String fetchUserData(String userId) {
// This blocks, but only the virtual thread, not an OS thread
// Network call here...
return "{\"id\":\"" + userId + "\"}";
}
static String fetchPermissions(String userId) {
// Another blocking call...
return "[\"read\",\"write\"]";
}
}5. Virtual Threads vs Traditional Architecture
Traditional Reactive/Async Architecture:
// Reactive/async code is complex - hard to read and debug
public CompletableFuture<String> getUserDataReactive(String userId) {
return fetchUserAsync(userId)
.thenCompose(user ->
fetchPermissionsAsync(userId)
.thenCombine(
fetchRolesAsync(userId),
(perms, roles) -> user + "," + perms + "," + roles
)
)
.exceptionally(e -> "Error: " + e.getMessage());
}Virtual Threads - Simple and Readable:
// Simple imperative code - easy to read and debug!
public String getUserData(String userId) {
String user = fetchUser(userId); // Blocking
String perms = fetchPermissions(userId); // Blocking
String roles = fetchRoles(userId); // Blocking
return user + "," + perms + "," + roles;
}
// But scales like async code!Developer Impact
Paradigm Shift:
- Before: Had to write complex async/reactive code
- After: Simple blocking code that scales to millions
Benefits:
- 100x-1000x more throughput for I/O-bound apps
- Simple, readable imperative code
- Existing libraries work perfectly
- Debugging is straightforward
Pros and Cons
Pros ✅
- Revolutionary Scalability: Millions of threads with minimal overhead
- Simple Code: Imperative blocking code remains readable
- Better Debugging: Stack traces show entire call chain
- Compatibility: Works with existing code
- Platform Benefit: Leverages multi-core processors efficiently
- Structured Concurrency: Better task coordination
Cons ❌
- Preview Status: Still experimental, may change
- Learning Curve: Different mental model
- Platform Dependent: Performance varies by OS
- Tooling: Debuggers may need updates
- Memory Overhead: Still uses memory (just less)
Conclusion
Virtual threads are transformative for Java concurrency. They eliminate the need for complex reactive frameworks for most use cases. I/O-bound applications can now handle millions of concurrent connections with simple, imperative code. This is among the most significant additions to Java in years.
Recommendation:
- Understand virtual threads thoroughly
- Prepare to migrate I/O-bound services
- Virtual threads are finalized in Java 21 LTS
- This will reshape how Java servers are built