Mastering Java Concurrency with Virtual Threads
Virtual threads representam uma revolução na forma como escrevemos código concorrente em Java. Descubra como essa feature do Project Loom simplifica o desenvolvimento de aplicações altamente concorrentes.
O Problema com Threads Tradicionais
Platform threads (threads tradicionais do Java) são wrappers finos sobre threads do sistema operacional. Isso significa que são recursos caros e limitados.
// Abordagem tradicional - cara e limitada
ExecutorService executor = Executors.newFixedThreadPool(200);
// Submeter 10.000 tarefas
for (int i = 0; i < 10000; i++) {
final int taskId = i;
executor.submit(() -> {
// Apenas 200 threads podem executar simultaneamente
performIOTask(taskId);
});
}
// Problema: 9.800 tarefas aguardando em fila!Virtual Threads: A Solução
Virtual threads são threads leves gerenciadas pela JVM, não pelo SO. Você pode criar milhões delas sem se preocupar com overhead.
Criando Virtual Threads
// Método 1: Thread.startVirtualThread()
Thread.startVirtualThread(() -> {
System.out.println("Running in virtual thread!");
});
// Método 2: Thread.ofVirtual()
Thread vThread = Thread.ofVirtual()
.name("worker-", 0)
.start(() -> performTask());
// Método 3: Executors.newVirtualThreadPerTaskExecutor()
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
for (int i = 0; i < 1_000_000; i++) {
executor.submit(() -> performIOTask());
}
} // Aguarda conclusão automaticamenteComo Virtual Threads Funcionam
Virtual threads usam carrier threads (platform threads) para executar. Quando uma virtual thread bloqueia em I/O, ela é desmontada do carrier thread, permitindo que outra virtual thread execute.
// Virtual thread blocando em I/O
Thread.startVirtualThread(() -> {
// Quando esta chamada bloqueia...
String data = httpClient.get("https://api.example.com/data");
// ...a virtual thread é desmontada do carrier thread
// Outras virtual threads podem usar o carrier thread
// Quando I/O completa, a virtual thread remonta e continua
processData(data);
});Casos de Uso Ideais
1. Servidores Web de Alta Concorrência
@RestController
public class UserController {
// Cada requisição roda em sua própria virtual thread
@GetMapping("/users/{id}")
public User getUser(@PathVariable Long id) {
// Chamadas I/O bloqueantes são OK!
User user = userRepository.findById(id);
List<Order> orders = orderService.getOrdersByUser(id);
UserProfile profile = profileService.getProfile(id);
return enrichUser(user, orders, profile);
}
}2. Processamento Paralelo de Dados
public class DataProcessor {
public void processFiles(List<Path> files) {
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
List<Future<Result>> futures = files.stream()
.map(file -> executor.submit(() -> processFile(file)))
.toList();
// Aguardar todos os resultados
for (Future<Result> future : futures) {
Result result = future.get();
saveResult(result);
}
} catch (Exception e) {
log.error("Processing failed", e);
}
}
private Result processFile(Path file) {
// I/O bound - perfeito para virtual threads
String content = Files.readString(file);
return analyzeContent(content);
}
}3. Tasks Agendadas
public class ScheduledTaskExecutor {
private final ScheduledExecutorService scheduler =
Executors.newScheduledThreadPool(1);
public void scheduleWithVirtualThreads() {
scheduler.scheduleAtFixedRate(() -> {
// Criar virtual thread para cada execução
Thread.startVirtualThread(() -> {
// Task pesada aqui
performBackup();
cleanupOldData();
sendNotifications();
});
}, 0, 1, TimeUnit.HOURS);
}
}Structured Concurrency
Structured Concurrency é um padrão que trata múltiplas tarefas como uma unidade única de trabalho, melhorando confiabilidade e observabilidade.
public class UserService {
public UserData fetchUserData(Long userId) throws Exception {
try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
// Lançar múltiplas tarefas concorrentes
Future<User> userFuture =
scope.fork(() -> userRepository.findById(userId));
Future<List<Order>> ordersFuture =
scope.fork(() -> orderRepository.findByUserId(userId));
Future<Profile> profileFuture =
scope.fork(() -> profileRepository.findByUserId(userId));
// Aguardar todas completarem
scope.join();
// Lançar se qualquer uma falhou
scope.throwIfFailed();
// Obter resultados
return new UserData(
userFuture.resultNow(),
ordersFuture.resultNow(),
profileFuture.resultNow()
);
}
}
}Cuidados e Armadilhas
1. Thread-Local Variables
Use com cautela - pode causar memory leaks com milhões de virtual threads:
// ❌ Evite ThreadLocal com virtual threads
private static final ThreadLocal<User> CURRENT_USER = new ThreadLocal<>();
// ✅ Use Scoped Values (Java 21+)
private static final ScopedValue<User> CURRENT_USER = ScopedValue.newInstance();
public void processRequest(User user) {
ScopedValue.where(CURRENT_USER, user)
.run(() -> handleRequest());
}2. Synchronized Blocks
Synchronized blocks podem pinar virtual threads, reduzindo escalabilidade:
// ❌ Evite synchronized em código de virtual thread
public synchronized void updateCounter() {
counter++;
}
// ✅ Use ReentrantLock ou outras primitivas java.util.concurrent
private final Lock lock = new ReentrantLock();
public void updateCounter() {
lock.lock();
try {
counter++;
} finally {
lock.unlock();
}
}3. CPU-Bound Tasks
Virtual threads são para I/O-bound tasks. Para CPU-bound, use ForkJoinPool:
// ❌ Não use virtual threads para cálculos pesados
Thread.startVirtualThread(() -> {
// CPU-bound - desperdiça recursos
long result = calculatePrimes(1_000_000);
});
// ✅ Use ForkJoinPool para CPU-bound
ForkJoinPool.commonPool().submit(() -> {
long result = calculatePrimes(1_000_000);
return result;
});Benchmarks e Performance
Resultados de testes reais comparando platform threads vs virtual threads:
- Throughput: 10-100x mais requisições/segundo para workloads I/O-bound
- Latência: Redução de 50-80% no tempo de resposta sob alta carga
- Uso de Memória: Pode criar 1M+ virtual threads com <1GB heap
- Escalabilidade: Linear scaling até milhões de threads concorrentes
Quando Usar Virtual Threads
✅ Use Virtual Threads Para:
- Operações I/O-bound (database, network, file I/O)
- Web servers com muitas requisições concorrentes
- Processamento paralelo de muitas tarefas pequenas
- Simplificar código assíncrono complexo
❌ Não Use Virtual Threads Para:
- Tarefas CPU-intensive (use ForkJoinPool)
- Código com muitos synchronized blocks
- Quando já usa reactive programming efetivamente
- Pools de threads compartilhados (crie on-demand)
Conclusão
Virtual threads são um game-changer para programação concorrente em Java. Elas permitem escrever código simples e imperativo que escala para milhões de operações concorrentes, sem a complexidade de programação assíncrona ou reactive.
Para a maioria das aplicações I/O-bound, virtual threads oferecem a melhor combinação de simplicidade, performance e escalabilidade. É hora de repensar como escrevemos código concorrente em Java!