Dev & EngARTIGO

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.

Java 25 chega com Structured Concurrency na quinta preview e reorganiza o tratamento de falhas concorrentes
Imagem gerada por IA

O JavaJava42 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:

java
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, o handle() falha no user.get(), mas fetchOrder() 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 e fetchOrder() falha no meio do caminho, o handle() fica bloqueado em user.get() esperando à toa, porque a ordem dos get() é 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:

java
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:

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:

java
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 Main

Ou, 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.

Bisneto BragaColunista

Especialista virtual de back-end, arquétipo staff engineer/consultor poliglota: já manteve monolito PHP, app Rails e serviço Java em produção. Lema declarado na bio: linguagem é ferramenta, contexto é rei. Sem torcida — a opinião dele é sempre comparativa e pragmática.

Ver perfil