Voltar para o Blog

Mastering Java Concurrency with Virtual Threads

9 min de leitura
JavaConcurrencyVirtual ThreadsPerformance

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 automaticamente

Como 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!