1. Introdução
Quando as threads tocam cada uma a sua parte
A concorrência tradicional muitas vezes lembra um ensaio sem maestro. Cada thread é como um músico que toca sua melodia sem ouvir os outros. Alguém terminou antes e foi fumar, alguém ficou preso em um único acorde, alguém simplesmente confundiu as notas e gerou um erro. O resultado não é uma sinfonia, mas uma cacofonia: é quase impossível entender onde alguém se perdeu, e parar todos de uma vez — é um verdadeiro desafio.
Structured Concurrency resolve esse problema. Ela transforma threads dispersas em um verdadeiro conjunto: todas as tarefas ficam sob um único “maestro”. Se ele manda parar — a orquestra silencia. Se um músico vacila — os demais param com cuidado, sem quebrar a harmonia geral. Todos os resultados e erros são coletados de forma centralizada, e não espalhados pelos cantos do código.
Imagine: você não deixa os músicos tocarem cada um do seu jeito, mas os reúne em uma única sala. Há um maestro, há a partitura e, mesmo que o trompete desafine, a orquestra não desmorona — conclui a apresentação com elegância.
O que o Structured Concurrency oferece
- Escopo único da tarefa: todas as subtarefas vivem dentro de um único bloco de código; seu ciclo de vida é limitado por esse bloco.
- Término previsível: a thread pai não termina até que todas as subtarefas terminem.
- Cancelamento centralizado: se uma tarefa falhar ou a thread pai decidir terminar — todas as subtarefas são canceladas corretamente.
- Tratamento consistente de erros: os erros das subtarefas são agregados; é possível obter uma “árvore de causas” (tree of causes).
- Código limpo e legível: não há threads “penduradas”, tarefas esquecidas ou corrida para cancelar.
Structured Concurrency não é apenas uma nova API, mas um novo modo de pensar: as tarefas devem ser estruturadas assim como blocos de código comuns (por exemplo, try-with-resources).
2. Status do Structured Concurrency no Java
No momento da escrita deste curso, Structured Concurrency está no status de Preview (Java 21–23), mas espera-se sua evolução para GA (General Availability) no Java 24/25. A API está no pacote jdk.incubator.concurrent. Antes de usar em produção, verifique as notas de versão atuais da sua versão do JDK!
Principais classes:
- StructuredTaskScope — classe base para gerenciar um grupo de tarefas.
- Variações: StructuredTaskScope.ShutdownOnFailure, StructuredTaskScope.ShutdownOnSuccess — políticas de término das tarefas.
Conceitos principais de StructuredTaskScope
Modelo: fork, join e uma checagem amigável dos resultados
Quando o maestro (isto é, a tarefa pai) dá o sinal — as subtarefas saem para executar suas partes. Esse momento é chamado de fork — como se você enviasse os músicos para tocar seus trechos em salas diferentes.
Depois chega a hora do join — o maestro levanta a batuta e todos retornam para tocar o acorde final juntos.
E então você pode perguntar a cada participante como tudo foi:
- usar resultNow() para obter o resultado imediatamente, se tudo foi executado sem erros;
- usar throwIfFailed() — para se certificar de que ninguém desafinou. Se alguém se confundiu nas notas, é lançada uma exceção única — como se o maestro dissesse: “Tivemos uma falha na orquestra, vamos recomeçar”.
Políticas de término
Todo maestro tem sua própria regra de quando parar a música. No Structured Concurrency isso é definido por uma política de término:
- ShutdownOnFailure — se ao menos um músico sai do ritmo, o maestro faz um gesto: “Pare! Vamos começar de novo”. Todos os demais param de tocar imediatamente.
- ShutdownOnSuccess — ao contrário, assim que alguém executa sua parte perfeitamente, o maestro fica satisfeito: “Basta, não precisamos ir além, já temos um vencedor”. Os demais se calam — política do primeiro resultado bem-sucedido.
Trabalhando com threads virtuais
Cada subtarefa StructuredTaskScope é executada em uma thread virtual. É como ter uma orquestra em que cada músico é prestativo e rápido, sem caprichos nem exigências de palco. Você pode criar centenas, milhares desses executores sem medo — não são threads pesadas, mas “notas” quase sem peso que soam exatamente quando necessário.
3. Exemplo: agregador de requisições HTTP
Vamos considerar uma tarefa prática: temos três fontes de dados (por exemplo, três servidores diferentes) e queremos obter a resposta ou de todas (e agregá-las) ou da primeira que responder com sucesso.
Variação 1: “Todos devem ter sucesso” (ShutdownOnFailure)
import jdk.incubator.concurrent.StructuredTaskScope;
import java.util.concurrent.Future;
public class AggregatorAllSuccess {
public static void main(String[] args) throws Exception {
try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
Future<String> f1 = scope.fork(() -> fetchFromSource1());
Future<String> f2 = scope.fork(() -> fetchFromSource2());
Future<String> f3 = scope.fork(() -> fetchFromSource3());
scope.join(); // aguardamos a conclusão de todas as tarefas
scope.throwIfFailed(); // se ao menos uma falhar — lançamos uma exceção
// Todas as tarefas tiveram sucesso — podemos agregar os resultados
String result = f1.resultNow() + f2.resultNow() + f3.resultNow();
System.out.println("Resultado agregado: " + result);
}
}
static String fetchFromSource1() { /* ... */ return "A"; }
static String fetchFromSource2() { /* ... */ return "B"; }
static String fetchFromSource3() { /* ... */ return "C"; }
}
O que acontece:
- As três tarefas são iniciadas em paralelo (em threads virtuais).
- Se pelo menos uma falhar — as demais são canceladas e uma exceção é lançada.
- Se todas tiverem sucesso — é seguro agregar os resultados.
Variação 2: “Sucesso pelo primeiro válido” (ShutdownOnSuccess)
import jdk.incubator.concurrent.StructuredTaskScope;
import java.util.concurrent.Future;
public class AggregatorFirstSuccess {
public static void main(String[] args) throws Exception {
try (var scope = new StructuredTaskScope.ShutdownOnSuccess<String>()) {
Future<String> f1 = scope.fork(() -> fetchFromSource1());
Future<String> f2 = scope.fork(() -> fetchFromSource2());
Future<String> f3 = scope.fork(() -> fetchFromSource3());
scope.join(); // aguardamos o primeiro bem-sucedido
scope.throwIfFailed(); // se todos falharem — lançamos uma exceção
String result = scope.result(); // resultado da primeira tarefa bem-sucedida
System.out.println("Primeiro resultado bem-sucedido: " + result);
}
}
static String fetchFromSource1() { /* ... */ return "A"; }
static String fetchFromSource2() { /* ... */ return "B"; }
static String fetchFromSource3() { /* ... */ return "C"; }
}
O que acontece:
- Assim que uma tarefa termina com sucesso — as outras são canceladas.
- Se todas falharem — uma exceção é lançada.
4. Cancelamento automático e degradação
StructuredTaskScope cuida do cancelamento das tarefas restantes, quando a política assim exige. Por exemplo, se uma tarefa falhar (ShutdownOnFailure) ou se uma terminar com sucesso (ShutdownOnSuccess), as demais recebem um sinal de cancelamento (interrupt).
Exemplo: término correto com tempo limite (timeout)
import jdk.incubator.concurrent.StructuredTaskScope;
import java.time.Instant;
import java.util.concurrent.Future;
try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
Future<String> f1 = scope.fork(() -> fetchWithTimeout());
Future<String> f2 = scope.fork(() -> fetchWithTimeout());
scope.joinUntil(Instant.now().plusSeconds(2)); // esperamos no máximo 2 segundos
scope.throwIfFailed();
String result = f1.resultNow() + f2.resultNow();
System.out.println(result);
}
Se as tarefas não terminarem em 2 segundos — uma exceção será lançada e todas as tarefas serão canceladas.
5. Erros e tratamento de exceções
Como as exceções das subtarefas são roteadas para o escopo
Às vezes, durante o concerto, alguém ainda erra as notas — StructuredTaskScope não finge que nada aconteceu. Ele registra cuidadosamente quem tocou fora do tom e depois repassa ao maestro um relatório completo. Quando você chama throwIfFailed(), ele lança uma exceção agregada — algo como um relatório consolidado: “Aqui está a lista de quem desafinou hoje”. Se necessário, é possível expandir essa “árvore de causas” e ver quem exatamente falhou. E se você quiser saber sobre um executante específico — Future.exceptionNow() dirá como a sua parte terminou.
Quando o cancelamento não é falha
É importante lembrar: cancelar uma tarefa nem sempre significa erro. Se o maestro disse “pronto, concerto encerrado”, então os músicos apenas guardam seus instrumentos — isso é cancelled, não failed. Erro é apenas a situação em que alguém realmente tocou algo errado, e essa exceção entrará no relatório geral.
Exemplo: árvore de causas
import jdk.incubator.concurrent.StructuredTaskScope;
import java.util.concurrent.Future;
try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
Future<String> f1 = scope.fork(() -> { throw new RuntimeException("Erro 1"); });
Future<String> f2 = scope.fork(() -> { throw new RuntimeException("Erro 2"); });
scope.join();
scope.throwIfFailed(); // lançará uma exceção com ambas as causas
} catch (Exception e) {
e.printStackTrace();
// É possível obter as exceções suprimidas via e.getSuppressed()
}
6. Comparação com CompletableFuture
StructuredTaskScope e CompletableFuture — ambos permitem executar tarefas em paralelo, mas:
- StructuredTaskScope é útil quando as tarefas estão logicamente relacionadas e devem terminar/cancelar juntas (hierarquia de tarefas).
- CompletableFuture é bom para composição de tarefas sem hierarquia (por exemplo, cadeias de transformações, cenários reativos).
Quando StructuredTaskScope simplifica o código:
- Quando é necessário garantir que todas as subtarefas terminaram antes de sair do bloco.
- Quando é preciso cancelamento centralizado e tratamento de erros.
- Quando é importante não deixar tarefas “penduradas”.
Quando CompletableFuture é mais conveniente:
- Quando as tarefas não estão relacionadas e podem viver por conta própria.
- Quando é necessária uma composição complexa (thenCombine, thenCompose etc.).
7. Prática: agregador de requisições HTTP
Tarefa: enviar requisições para 3 fontes e obter a primeira resposta bem-sucedida
import jdk.incubator.concurrent.StructuredTaskScope;
import java.util.concurrent.Future;
public class HttpAggregator {
public static void main(String[] args) throws Exception {
try (var scope = new StructuredTaskScope.ShutdownOnSuccess<String>()) {
Future<String> f1 = scope.fork(() -> httpRequest("https://api1.example.com"));
Future<String> f2 = scope.fork(() -> httpRequest("https://api2.example.com"));
Future<String> f3 = scope.fork(() -> httpRequest("https://api3.example.com"));
scope.join();
scope.throwIfFailed();
String result = scope.result();
System.out.println("Primeira resposta bem-sucedida: " + result);
}
}
static String httpRequest(String url) throws Exception {
// Simulação de requisição (você pode usar HttpClient)
Thread.sleep((long) (Math.random() * 1000));
if (Math.random() < 0.3) throw new RuntimeException("Erro de requisição: " + url);
return "Resposta de " + url;
}
}
Tarefa: se uma subtarefa falhar — desligamos corretamente as demais
import jdk.incubator.concurrent.StructuredTaskScope;
import java.util.concurrent.Future;
try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
Future<String> f1 = scope.fork(() -> httpRequest("https://api1.example.com"));
Future<String> f2 = scope.fork(() -> httpRequest("https://api2.example.com"));
scope.join();
scope.throwIfFailed();
String result = f1.resultNow() + f2.resultNow();
System.out.println("Ambas as respostas: " + result);
} catch (Exception e) {
System.err.println("Erro em uma das tarefas: " + e.getMessage());
}
8. Erros típicos ao trabalhar com StructuredTaskScope
Erro nº 1: esqueceu de chamar join() ou throwIfFailed().
Se você não chamar join(), as tarefas podem não terminar antes de sair do bloco. Se não chamar throwIfFailed(), os erros das subtarefas passarão despercebidos.
Erro nº 2: tentar obter o resultado antes da conclusão da tarefa.
Chamar resultNow() antes da conclusão lança IllegalStateException. Primeiro aguarde a conclusão com join().
Erro nº 3: ignorar o cancelamento.
Se a tarefa foi cancelada (por exemplo, devido à política do escopo), não tente obter seu resultado — uma exceção será lançada.
Erro nº 4: misturar diferentes políticas de término.
Não tente cancelar tarefas manualmente dentro do escopo — use as políticas ShutdownOnFailure ou ShutdownOnSuccess.
Erro nº 5: executar tarefas longas de CPU em threads virtuais.
StructuredTaskScope usa threads virtuais por padrão — elas são ideais para tarefas I/O-bound, mas não aceleram cálculos pesados.
Erro nº 6: esquecer de fechar o escopo (sem try-with-resources).
StructuredTaskScope implementa AutoCloseable — sempre use try-with-resources para garantir o término de todas as tarefas.
GO TO FULL VERSION