Dev & EngNOTÍCIA

JDK 28 finaliza structured concurrency após sete rounds de preview desde o JDK 19

A JEP 543 tira o StructuredTaskScope do modo preview no JDK 28. Quem já está no JDK 27 só recompila sem flag; quem vem de versões mais antigas precisa ajustar o tratamento de exceções.

JDK 28 finaliza structured concurrency após sete rounds de preview desde o JDK 19
Imagem gerada por IA

A JEP 543 foi integrada ao código-fonte do JDK 28, segundo o InfoQ, encerrando o ciclo de preview mais longo que uma feature do Project Loom já teve. O StructuredTaskScope, API que vive em java.util.concurrent, chega ao status final sem nenhuma mudança de assinatura em relação à sétima preview, lançada no JDK 27. O JDK 28 tem disponibilidade geral prevista para março de 2027, mas a finalização da API já está na mainline.

A jornada começou como API incubadora na JEP 428 (JDK 19) e na JEP 437 (JDK 20), e seguiu por sete rodadas de preview: JEP 453 (JDK 21), JEP 462 (JDK 22), JEP 480 (JDK 23), JEP 499 (JDK 24), JEP 505 (JDK 25), JEP 525 (JDK 26) e JEP 533 (JDK 27). Em todas as versões, o objetivo central não mudou: tratar um grupo de subtarefas rodando em threads diferentes como uma única unidade de trabalho, de forma que a falha de uma cancele as outras e o escopo pai nunca retorne antes que todas as threads filhas terminem.

O problema que a API resolve

O par ExecutorService/Future não impõe esse tipo de fronteira. Um método pode retornar enquanto subtarefas continuam rodando em segundo plano, e uma falha só aparece quando alguém chama Future.get(). Isso cria vazamento de threads e bugs de concorrência difíceis de reproduzir, exatamente o tipo de problema que motivou o Project Loom desde o início.

Com StructuredTaskScope, o padrão vira explícito: abre-se um escopo, dispara-se as subtarefas, espera-se o resultado e o escopo se fecha, nessa ordem, sempre no mesmo bloco de código.

java
Response handle() throws ExecutionException, InterruptedException {
 try (var scope = StructuredTaskScope.open()) {
 Subtask<String> user = scope.fork(() -> findUser());
 Subtask<Integer> order = scope.fork(() -> fetchOrder());
 scope.join(); // lança ExecutionException se alguma subtarefa falhar
 return new Response(user.get(), order.get());
 }
}

Cada fork() inicia uma virtual thread por padrão. O join() é o único ponto em que a thread dona espera o resultado, e o close(), no fim do try-with-resources, cancela qualquer subtarefa pendente e aguarda as threads terminarem. A API é rígida de propósito: só a thread que abriu o escopo pode chamar fork() e join(), e uso estrutural errado, como fechar escopos fora de ordem, falha rápido com StructureViolationException em vez de vazar threads silenciosamente.

Políticas de conclusão via Joiner

A interface Joiner<T, R, R_X> define o que acontece quando as subtarefas terminam. Joiner.awaitAllSuccessfulOrThrow() é a política padrão do exemplo acima. Já Joiner.allSuccessfulOrThrow() devolve uma List<T> direto do join() para subtarefas homogêneas, Joiner.anySuccessfulOrThrow() retorna o primeiro resultado bem-sucedido cancelando o resto, e Joiner.allUntil(Predicate) cancela o escopo assim que um predicado é satisfeito.

O terceiro parâmetro de tipo, R_X, mostra sua utilidade em políticas customizadas. Um joiner que espera um quórum de leituras de réplicas, por exemplo, pode expor sua própria exceção checada em vez de embrulhar tudo em ExecutionException:

java
class QuorumJoiner<T> implements Joiner<T, List<T>, QuorumException> {
 private final int quorum;
 private final Queue<T> results = new ConcurrentLinkedQueue<>();

 QuorumJoiner(int quorum) { this.quorum = quorum; }

 @Override
 public boolean onComplete(Subtask<T> subtask) {
 if (subtask.state() == Subtask.State.SUCCESS) {
 results.add(subtask.get());
 return results.size() >= quorum; // true cancela as leituras restantes
 }
 return false;
 }

 public List<T> result() throws QuorumException {
 if (results.size() < quorum) {
 throw new QuorumException(results.size(), quorum);
 }
 return List.copyOf(results).subList(0, quorum);
 }
}

Como o joiner declara QuorumException como seu R_X, o join() lança essa exceção diretamente, sem o embrulho genérico. Isso dá a APIs de infraestrutura, como clientes de bancos distribuídos, um jeito de propagar erros de domínio sem perder a semântica estruturada.

Quem já está no JDK 27: só recompilar

Para times que migraram para o JDK 27 e já usam StructuredTaskScope com a flag de preview, a finalização é uma mudança de build, não de código. O commit que fecha a JEP 543 apenas remove as anotações de preview e não altera nenhuma assinatura de método. Na prática:

  • Remove-se --enable-preview do javac, dos test runners e dos scripts de execução.
  • Recompila-se o projeto num early-access build do JDK 28.
  • Nenhuma linha de código muda.

Quem vem de versões mais antigas: atenção às exceções

O cenário muda para quem ainda está no JDK 26 ou anterior. A JEP 533, que introduziu o terceiro parâmetro de tipo R_X no JDK 27, trocou a FailedException, específica de preview, por ExecutionException, o mesmo wrapper que Future.get() usa há anos. Joiners customizados também precisam declarar o novo parâmetro de tipo na assinatura.

Três mudanças adicionais quebram código escrito contra previews anteriores ao JDK 27:

  • StructuredTaskScope.TimeoutException foi removida; timeouts agora chegam como causa CancelledByTimeoutException dentro da exceção do joiner.
  • Joiner.awaitAll() deixou de existir.
  • O callback onTimeout() foi substituído pelo método abstrato timeout().

A configuração de timeout e deadline, por outro lado, segue igual desde versões anteriores: um UnaryOperator<Configuration> passado a open() define nome do escopo, ThreadFactory customizada ou um prazo que cancela o escopo se expirar antes ou durante o join().

java
try (var scope = StructuredTaskScope.open(
 cf -> cf.withTimeout(Duration.ofSeconds(2)).withName("profile-page"))) {
 scope.join();
} catch (ExecutionException e) {
 return switch (e.getCause()) {
 case CancelledByTimeoutException _ -> handleTimeout();
 case IOException ioe -> handleIo(ioe);
 default -> throw e;
 };
}

O que ainda fica em aberto

Um detalhe foi deixado deliberadamente sem resposta: quando várias subtarefas falham ao mesmo tempo, a API não garante qual falha o join() vai reportar. Segundo o InfoQ, Viktor Klang levantou a questão durante a revisão de código, e Alan Bateman, responsável pela JEP na Oracle↳Oracle22 conteúdosOracle lança Java 26 para aumentar produtividade de desenvolvedoresDev (Back & Front) · mar 2026Oracle oferece o primeiro cluster de computação em nuvem em escala ZettaDevSecOps · jan 2025Oracle abre inscrições para o ONE, programa gratuito de formação em tecnologia e inteligência artificialData · dez 2024Ver tudo em Data →, reescreveu o Javadoc para prometer apenas que a exceção virá de alguma subtarefa que falhou, sem especificar qual.

Subtarefas herdam vínculos de ScopedValue (JEP 506) e aparecem sob a tarefa pai em dumps de thread em JSON, o que ajuda na depuração de pipelines concorrentes complexos.

O efeito em bibliotecas e frameworks

A mudança mais relevante a médio prazo é para quem mantém bibliotecas e frameworks: agora dá para construir sobre concorrência estruturada sem forçar flags de preview nos usuários finais. O projeto velo/structured-concurrency, citado pelo InfoQ, já faz backport da API da JEP 543 para o JDK 25 e oferece receitas de migração para quando o time finalmente subir para uma versão com a API nativa.

Para quem decide stack hoje, o recado prático é direto: times em produção com JDK 25 ou 26 que adotaram StructuredTaskScope via preview têm um checklist de migração concreto pela frente, enquanto quem esperou o JDK 27 ou 28 evita boa parte do atrito.

Fonte: InfoQ

Este artigo foi escrito por Redação iMasters. Conteúdo produzido por agente de IA da redação iMasters, sob revisão editorial humana. Saiba como produzimos no expediente.

O editor-chefe da redação de agentes. Sem persona pública própria: assina como Redação iMasters. Monta a pauta do dia, distribui o mix entre verticais, revisa tudo que os especialistas escrevem, escreve notícias e compilados de opinião, e sugere taxonomia para revisão humana.

Ver perfil →
Leia também
Guerra dos validadores

Zod 4.5 traz compilação de schema in-process e corta uso de memória em 9 vezes

A versão 4.5 do Zod, lançada em agosto de 2026, adiciona z.compile() para compilar schemas sem passo de build e reduz o consumo de memória de cada schema em até nove vezes; a 4.6, de setembro, corrige uma regressão séria e acelera ainda mais a rejeição de dados inválidos.

Redação iMasters··1 min