Java 25 chega com Structured Concurrency na quinta preview e reorganiza o tratamento de falhas concorrentes
O JEP 505 troca ExecutorService e Future por StructuredTaskScope aberto via factory methods. Veja o código antes e depois, e o que ainda é preview.

O Java↳Java42 conteúdosNovidades do Java 26 (para desenvolvedores)Dev (Back & Front) · mar 2026Visual Studio Code para Java: o guia completo (dicas, configuração e extensões)Dev (Back & Front) · out 2025Quarkus: Modernizando a linguagem Java para a era da nuvemDev (Back & Front) · nov 2020Ver tudo em Dev (Back & Front) → 25 saiu como LTS e trouxe, entre outras coisas, a quinta preview da Structured Concurrency via JEP 505. A API não é nova, ela incubou lá no JDK 19 e vem sendo repreviewada release após release, mas a rodada atual mexe na assinatura principal e vale a pena entender o que muda para quem tem serviço Java em produção fazendo fan-out de chamadas de I/O.
O problema que a proposta ataca é velho e conhecido de qualquer um que já orquestrou chamadas concorrentes na mão: ExecutorService e Future dão liberdade demais e estrutura de menos. Você submete tarefas, guarda os futures, dá get() em cada um, e a relação lógica entre a tarefa pai e as subtarefas existe só na sua cabeça, não no runtime.
O que quebra com ExecutorService
O exemplo canônico do próprio JEP é um handler que busca usuário e pedido em paralelo:
Response handle() throws ExecutionException, InterruptedException {
Future<String> user = executor.submit(() -> findUser());
Future<Integer> order = executor.submit(() -> fetchOrder());
String theUser = user.get(); // Join findUser
int theOrder = order.get(); // Join fetchOrder
return new Response(theUser, theOrder);
}Parece inofensivo, mas o JEP lista três armadilhas concretas:
- Se
findUser()estoura exceção, ohandle()falha nouser.get(), masfetchOrder()continua rodando na thread dele. É thread leak: no melhor caso desperdiça recurso, no pior atrapalha outras tarefas. - Se a thread do
handle()é interrompida, a interrupção não propaga para as subtarefas. As duas vazam. - Se
findUser()demora efetchOrder()falha no meio do caminho, ohandle()fica bloqueado emuser.get()esperando à toa, porque a ordem dosget()é fixa e não reage à falha da outra subtarefa.
Dá pra remendar tudo isso com try-finally, cancel(boolean) nos futures dentro do catch, e ExecutorService dentro de try-with-resources. Mas, como o texto do JEP reconhece, esse malabarismo de coordenar lifetimes manualmente "pode ser difícil de acertar, e muitas vezes torna a intenção lógica do código mais difícil de perceber". A raiz é que Future pode ser joinado por qualquer thread que tenha a referência, inclusive uma que nunca submeteu nada, então o runtime não tem como impor relação pai-filho.
Como fica com StructuredTaskScope
A mesma lógica com a API nova:
Response handle() throws InterruptedException {
try (var scope = StructuredTaskScope.open()) {
Subtask<String> user = scope.fork(() -> findUser());
Subtask<Integer> order = scope.fork(() -> fetchOrder());
scope.join(); // Junta as subtarefas, propagando exceções
return new Response(user.get(), order.get());
}
}A diferença central: o lifetime das threads está confinado ao bloco do try-with-resources. Fora dali, elas não existem. E o StructuredTaskScope.open() sem parâmetros já vem com a política padrão que resolve as três armadilhas de uma vez:
- Short-circuiting — se
findUser()oufetchOrder()falha, a outra é cancelada (interrompida) automaticamente, se ainda não terminou. - Propagação de cancelamento — se a thread do
handle()é interrompida antes ou durante ojoin(), as duas subtarefas são canceladas na saída do escopo. - Observabilidade↳Observabilidade11 conteúdosObservabilidade para APIs: os desafios e benefícios dessa abordagemDev (Back & Front) · jan 2025Falhas em Observabilidade afetam os Apps e a Segurança das OrganizaçõesDev (Back & Front) · nov 2023ADK Java 1.0: O Google quer que você pare de gambiarra Python no seu backendMarketing Tech · abr 2026Ver tudo em DevSecOps → — um thread dump mostra
findUser()efetchOrder()como filhas do escopo, não penduradas em threads soltas sem relação nenhuma.
Repare que fork() agora retorna um Subtask, não um Future (mudança que veio lá no JDK 21). E o Subtask::get() só pode ser chamado depois do join(), senão estoura exceção. Isso força o fluxo correto: forka tudo, junta como unidade, só então lê resultado.
A mudança desta preview: factory methods e Joiner
O que o JEP 505 muda em relação à quarta preview (JEP 499) é a forma de abrir o escopo. Antes você usava construtores públicos; agora, static factory methods:
public static <T> StructuredTaskScope<T, Void> open();
public static <T, R> StructuredTaskScope<T, R> open(Joiner<? super T, ? extends R> joiner);O open() de zero parâmetros cobre o caso comum (espera todas as subtarefas terem sucesso, ou falha se qualquer uma falhar). Para outras políticas, você passa um Joiner para a versão de um parâmetro. É aí que moram os comportamentos alternativos: esperar o primeiro resultado bem-sucedido e cancelar o resto, coletar todos os sucessos ignorando falhas, e por aí vai. A API ficou parametrizada em , onde T é o tipo das subtarefas e R o tipo do resultado do join(), o que dá mais expressividade do que a versão anterior baseada em subclasses fixas.
Cada fork() inicia uma thread que, por padrão, é virtual thread (JEP 444). É a combinação que dá sentido a tudo: virtual threads tornam barato dedicar uma thread por operação de I/O, e a Structured Concurrency coordena esse enxame sem vazamento. Uma subtarefa pode abrir o próprio escopo e forkar as dela, formando uma hierarquia de escopos que espelha o aninhamento sintático do código, o equivalente concorrente da call stack de uma thread só.
O que já é seguro usar e o que não é
Aqui vem o recado pragmático. O StructuredTaskScope continua sendo preview API, desabilitada por padrão. Para compilar e rodar você precisa dos flags:
javac --release 25 --enable-preview Main.java
java --enable-preview MainOu, com o source launcher, java --enable-preview Main.java; no jshell, jshell --enable-preview.
Na prática, isso significa que a assinatura ainda pode mudar, e de fato mudou entre previews (esta rodada trocou construtores por factory methods; a documentação já aponta o JEP 525 como sexta preview). Colocar isso na trilha crítica de um serviço em produção hoje é apostar que sua base vai acompanhar refatorações a cada release, e sem garantia de compatibilidade binária. Para código de produção que precisa de estabilidade, o par ExecutorService + coordenação manual, por mais chato que seja, continua sendo o caminho suportado.
Onde a preview compensa: projetos internos, POCs, código novo em times que já rodam Java atualizado e querem se antecipar ao padrão que quase certamente vira definitivo, e cenários de fan-out de I/O onde o thread leak do modelo antigo já causa dor real. O ganho de legibilidade e de observabilidade é concreto, e migrar depois tende a ser mecânico.
Um ponto de atenção que o próprio JEP levanta: cancelamento só funciona se as subtarefas respondem a interrupção. Se uma subtarefa bloqueia em método não interruptível, ela pode segurar o close() do escopo indefinidamente, porque o close() sempre espera todas as threads terminarem, mesmo com o escopo cancelado. Ou seja, a estrutura resolve a coordenação, mas não conserta código que ignora InterruptedException.
Vale reforçar o que a proposta não pretende: não é para substituir ExecutorService nem Future, que seguem existindo para os casos de concorrência não estruturada. Não é canal de dados entre threads, e não substitui o mecanismo de interrupção. É uma ferramenta para um padrão específico: tarefa que se divide em subtarefas e as junta no mesmo bloco. Dentro desse recorte, é onde faz mais sentido, e fora dele, os construtos antigos continuam sendo a escolha certa.
Fonte: OpenJDK JEP 505 — Structured Concurrency (Fifth Preview)
Este artigo foi escrito por Bisneto Braga, colunista de back-end. Conteúdo produzido por agente de IA da redação iMasters, sob revisão editorial humana. Saiba como produzimos no expediente.














