Project Loom Changes Everything
Java 21's virtual threads let you write blocking code that scales like async code. No reactive programming required.
Virtual Threads vs Platform Threads
// Platform thread (OS thread) — expensive
Thread platformThread = new Thread(() -> {
// Each thread uses ~1MB stack
doWork();
});
// Virtual thread — cheap (a few hundred bytes)
Thread virtualThread = Thread.ofVirtual().start(() -> {
// Mounted on platform thread only when running
doWork();
});
// Create millions of virtual threads
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
IntStream.range(0, 1_000_000)
.forEach(i -> executor.submit(() -> {
Thread.sleep(Duration.ofSeconds(1)); // Unmounts while sleeping!
return i;
}));
}
Spring Boot 3.2+ with Virtual Threads
# application.yml
spring:
threads:
virtual:
enabled: true # That's it!
// Verify virtual threads are active
@RestController
public class ThreadController {
@GetMapping("/thread-info")
public Map<String, Object> threadInfo() {
Thread current = Thread.currentThread();
return Map.of(
"name", current.getName(),
"isVirtual", current.isVirtual(),
"threadId", current.threadId()
);
}
}
Structured Concurrency (Preview)
import java.util.concurrent.StructuredTaskScope;
public record UserProfile(User user, List<Order> orders, List<Review> reviews) {}
public UserProfile fetchUserProfile(long userId) throws Exception {
try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
StructuredTaskScope.Subtask<User> userTask =
scope.fork(() -> userRepository.findById(userId));
StructuredTaskScope.Subtask<List<Order>> ordersTask =
scope.fork(() -> orderRepository.findByUserId(userId));
StructuredTaskScope.Subtask<List<Review>> reviewsTask =
scope.fork(() -> reviewRepository.findByUserId(userId));
scope.join().throwIfFailed(); // Wait for all, fail fast
return new UserProfile(
userTask.get(),
ordersTask.get(),
reviewsTask.get()
);
}
}
Scoped Values (Replacing ThreadLocal)
import java.lang.ScopedValue;
// Define scoped values
private static final ScopedValue<String> REQUEST_ID = ScopedValue.newInstance();
private static final ScopedValue<User> CURRENT_USER = ScopedValue.newInstance();
// Set in filter/middleware
public void handleRequest(HttpRequest request) {
String requestId = request.getHeader("X-Request-ID");
User user = authenticate(request);
ScopedValue.where(REQUEST_ID, requestId)
.where(CURRENT_USER, user)
.run(() -> processRequest(request));
}
// Access anywhere in the call stack
public void someDeepMethod() {
String requestId = REQUEST_ID.get();
User user = CURRENT_USER.get();
// No need to pass these as parameters!
}
JDBC with Virtual Threads
// Virtual threads shine with blocking JDBC
// Connection pool sizing: cores × 10 (not cores × 2)
@Configuration
public class DataSourceConfig {
@Bean
public DataSource dataSource() {
HikariConfig config = new HikariConfig();
config.setJdbcUrl("jdbc:postgresql://localhost/db");
config.setMaximumPoolSize(
Runtime.getRuntime().availableProcessors() * 10
);
config.setMinimumIdle(10);
return new HikariDataSource(config);
}
}
HTTP Client with Virtual Threads
// Java 11+ HttpClient is already async-friendly
// With virtual threads, you can use the synchronous API at scale
HttpClient client = HttpClient.newBuilder()
.version(HttpClient.Version.HTTP_2)
.connectTimeout(Duration.ofSeconds(5))
.build();
// This blocks but scales with virtual threads
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
List<CompletableFuture<String>> futures = urls.stream()
.map(url -> CompletableFuture.supplyAsync(() -> {
try {
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(url))
.GET()
.build();
HttpResponse<String> response = client.send(
request,
HttpResponse.BodyHandlers.ofString()
);
return response.body();
} catch (Exception e) {
throw new RuntimeException(e);
}
}, executor))
.toList();
List<String> results = futures.stream()
.map(CompletableFuture::join)
.toList();
}
Pinning Pitfalls
Virtual threads get "pinned" to platform threads when:
- Inside
synchronizedblocks - Using
nativemethods
// Bad: synchronized blocks pin virtual threads
public synchronized void doWork() {
Thread.sleep(1000); // Pins during sleep!
}
// Good: use ReentrantLock instead
private final ReentrantLock lock = new ReentrantLock();
public void doWork() {
lock.lock();
try {
Thread.sleep(1000); // Virtual thread unmounts!
} finally {
lock.unlock();
}
}
Benchmark Numbers
On a modern server with virtual threads:
- 10,000 concurrent requests with Spring Boot
- Response time: similar to reactive WebFlux
- Code complexity: 90% simpler than reactive
Virtual threads make Java competitive with Go's goroutines and Kotlin coroutines.