Explorando a simultaneidade em Java – de threads à simultaneidade estruturada | Geekware

A jornada do Java na simultaneidade tem sido longa e fascinante — evoluindo de threads gerenciados manualmente para threads virtuais leves e simultaneidade estruturada na era moderna. Nesta postagem, exploraremos diversas abordagens para executar tarefas simultaneamente, mostrando como cada etapa melhora a simplicidade, a legibilidade e a escalabilidade.
Você pode executar esses exemplos com Java 25 diretamente de .java arquivos de origem:
(!NOTE) Para o escopo desta postagem, não cobriremos os bons e velhos Threads (threads gerenciados pelo sistema operacional), mas começaremos diretamente usando Virtual Threads (threads gerenciados por JVM).
1. Começando com o básico – The Raw Thread
Aqui, só queremos executar uma tarefa simples simultaneamente usando um Thread (um virtual um). É o alicerce mais fundamental para simultaneidade em Java.
void main() throws InterruptedException {
Thread worker = Thread.startVirtualThread(() -> {
IO.println("Running in a separate thread: " + Thread.currentThread().getName());
});
worker.join();
IO.println("Main thread finished.");
}
O que está acontecendo:
- Criamos e iniciamos um novo tópico virtual para executar uma tarefa.
join()garante que o thread principal aguarde a conclusão.
2. Conjuntos de threads com ExecutorService
Neste exemplo, queremos execute várias tarefas com eficiência usando um conjunto de threads de trabalho. Isso evita a criação de muitos threads e nos permite gerenciar melhor os recursos.
import java.util.concurrent.*;
import java.util.stream.*;
void main() throws Exception {
try (var executor = Executors.newFixedThreadPool(4)) {
var futures = IntStream.range(0, 5)
.mapToObj(i -> executor.submit(() -> {
Thread.sleep(200);
return "Task " + i + " done by " + Thread.currentThread().getName();
}))
.toList();
for (var f : futures) IO.println(f.get());
}
}
O que está acontecendo:
- Enviamos 5 tarefas para um conjunto de 4 threads.
- Cada tarefa fica suspensa por um curto período e depois retorna um resultado.
- O thread principal recupera todos os resultados via
Future.get().
3. Pipelines assíncronos com CompletableFuture
Agora, vamos operações assíncronas em cadeia para simular a busca e o processamento de dados de maneira sem bloqueio.
import java.util.concurrent.*;
void main() {
try (var pool = Executors.newVirtualThreadPerTaskExecutor()) {
CompletableFuture.supplyAsync(() -> {
IO.println("Fetching data...");
sleep(300);
return "Data";
}, pool)
.thenApply(data -> {
IO.println("Processing " + data);
return data.length();
})
.thenAccept(len -> IO.println("Result length: " + len))
.join();
}
}
void sleep(long ms) {
try { Thread.sleep(ms); } catch (InterruptedException ignored) {}
}
O que está acontecendo:
CompletableFutureexecuta cada etapa de forma assíncrona.- Os resultados fluem de um estágio para o outro sem bloqueios.
- Esse padrão é poderoso para operações com uso pesado de IO.
4. Simultaneidade Estruturada – Subtarefas de Coordenação Limpa
Neste exemplo final, queremos iniciar duas subtarefas simultaneamenteespere que ambos terminem (ou cancele o outro se um falhar) e depois combine seus resultados. A simultaneidade estruturada fornece uma maneira mais segura e clara de lidar com diversas tarefas simultâneas como parte de uma única operação lógica.
import java.time.Instant;
import java.util.concurrent.StructuredTaskScope;
import java.util.concurrent.StructuredTaskScope.Subtask;
void main() throws Exception {
try (var scope = StructuredTaskScope.open()) {
Subtask<String> users = scope.fork(() -> loadData("users"));
Subtask<String> orders = scope.fork(() -> loadData("orders"));
// Wait for both subtasks to complete or for one to fail
scope.join();
// If join succeeded, both tasks completed successfully
IO.println("Results: %s | %s"
.formatted(users.get(), orders.get()));
}
}
String loadData(String type) throws InterruptedException {
IO.println("Loading %s...".formatted(type));
Thread.sleep(300);
return "Loaded %s at %s".formatted(type, Instant.now());
}
(!IMPORTANTE) ⚠️ Requer
--enable-previewsinalizador em Java 25.
O que está acontecendo:
StructuredTaskScope.open()cria um escopo para subtarefas simultâneas.- Cada subtarefa é digitada como
Subtaskpara maior clareza, usando a importação. fork(...)agenda uma subtarefa dentro do escopo.scope.join()espera que todas as tarefas terminem ou cancela outras se alguma falhar.- Os resultados são recuperados com segurança usando
get().
Há mais vantagens em usar a simultaneidade de estrutura, como cancelamento de escopo, que permite lidar com tarefas simultâneas complexas.
Usando Threads Virtuais no Spring Boot

Com os threads virtuais e a simultaneidade estruturada do Java 25, vimos como gerenciar com eficiência tarefas simultâneas em aplicativos independentes. Esses mesmos princípios podem ser aplicados em estruturas modernas como Spring Boot, onde threads de servidor que lidam com solicitações HTTP também podem aproveitar threads virtuais. Isso permite o processamento de solicitações escalonável e sem bloqueio, ao mesmo tempo que mantém o código legível e de fácil manutenção.
Spring Boot 3.2+ suporta o uso tópicos virtuais para lidar com solicitações HTTP e outras tarefas, aproveitando os recursos do Java 19+ Project Loom. Isso pode melhorar muito a escalabilidade para cargas de trabalho vinculadas a E/S.
Configurando Threads Virtuais
Habilitar threads virtuais no Spring é tão simples quanto incluir uma classe de configuração como a seguinte:
@EnableAsync
@Configuration
public class ThreadConfig {
@Bean
public AsyncTaskExecutor applicationTaskExecutor() {
return new TaskExecutorAdapter(Executors.newVirtualThreadPerTaskExecutor());
}
@Bean
public TomcatProtocolHandlerCustomizer protocolHandlerVirtualThreadExecutorCustomizer() {
return protocolHandler -> {
protocolHandler.setExecutor(Executors.newVirtualThreadPerTaskExecutor());
};
}
}
Agora, se você definir um novo controlador como o seguinte:
@RestController
@RequestMapping("/thread")
public class ThreadController {
@GetMapping("/name")
public String getThreadName() {
return Thread.currentThread().toString();
}
}
e consulte:
$ curl -s http://localhost:8080/thread/name
$ VirtualThread(#171)/runnable@ForkJoinPool-1-worker-4
você poderá validar se de fato a solicitação foi tratada por uma instância de um thread virtual.
Você pode encontrar um tutorial mais detalhado sobre Spring e Virtual Threads aqui: https://www.baeldung.com/spring-6-virtual-threads
Lições aprendidas
Cada etapa melhora a experiência do desenvolvedor, mas também explica quais problemas a solução mais avançada resolve:
- Tópicos: Simples, mas sujeito a erros para múltiplas tarefas.
- Executores: Gerencie recursos com eficiência.
- Futuro completável: Pipelines expressos de trabalho assíncrono.
- Simultaneidade Estruturada: Fornece escopo claro, cancelamento automático e tratamento simples de erros.
Conclusão
Java agora oferece uma rica caixa de ferramentas para programação simultânea:
- Tópicos são melhores para tarefas simples e únicas em segundo plano.
- Executores são ideais para controlar o número de threads ativos.
- CompletávelFuturo brilha para compor tarefas assíncronas dependentes.
- Simultaneidade Estruturada traz ordem e segurança a fluxos de trabalho simultâneos complexos, alinhando-se com a forma como os humanos pensam sobre tarefas e subtarefas.
Cada abordagem se baseia na anterior – culminando em um modelo onde a simultaneidade não é apenas eficiente, mas também estruturado, previsível e legível. O Java moderno nunca fez com que a simultaneidade parecesse tão acessível.
