正在加载,请稍候…

Java 21 Virtual Threads: Project Loom for High-Concurrency APIs

Leverage Java 21 virtual threads to handle millions of concurrent connections — thread per request model, structured concurrency, scoped values, and migrating from reactive code.

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:

  1. Inside synchronized blocks
  2. Using native methods
// 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.