JDK 25 LTS traz Scoped Values final e Structured Concurrency em quinta preview
A LTS de setembro consolida o JEP 506 como sucessor oficial do ThreadLocal e evolui o JEP 505 para a quinta rodada de preview. Veja como trocar um pelo outro num serviço real e onde a migração ainda trava.

O JDK 25 chegou à disponibilidade geral em 16 de setembro de 2025 e é a nova LTS da linha, segundo a página oficial de release do OpenJDK. Entre os 18 JEPs listados, dois interessam diretamente a quem mantém serviç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) → rodando em produção hoje: o JEP 506 (Scoped Values), que sai da preview e vira API definitiva, e o JEP 505 (Structured Concurrency), que chega à quinta rodada de preview. Não são features de vitrine como o Compact Object Headers (JEP 519) ou o Generational Shenandoah (JEP 521) \u2014 são mudanças que afetam diretamente como se escreve concorrência em Java, e por isso valem um olhar mais de perto, com código antes e depois.
Por que o ThreadLocal virou problema
ThreadLocal existe desde o Java 1.2 e resolveu um problema real: guardar contexto (usuário autenticado, id de transação, locale) sem passar parâmetro por todas as camadas de chamada. O problema apareceu com virtual threads (Project Loom, estabilizado no JDK 21): ThreadLocal foi desenhado para um mundo com poucas threads de plataforma, caras e de vida longa. Num serviço que agora cria milhões de virtual threads por dia, cada uma carregando seu próprio mapa de ThreadLocal, o custo de memória e o risco de vazamento (esquecer o remove() no finally) se tornam reais.
O Scoped Values ataca exatamente isso. A ideia central: o valor é imutável durante todo o escopo de execução, é compartilhado com threads filhas sem cópia, e não existe API de set() para mutar depois \u2014 só existe where().run() ou where().call(), que define o valor para a duração de um bloco.
Código antes: ThreadLocal num serviço típico
Um padrão comum em serviços que carregam contexto de request através de filtros, controllers e camadas de serviço:
private static final ThreadLocal<UserContext> CURRENT_USER = new ThreadLocal<>();
public void handleRequest(Request req) {
CURRENT_USER.set(loadUser(req));
try {
processRequest(req);
} finally {
CURRENT_USER.remove();
}
}
public void processRequest(Request req) {
UserContext user = CURRENT_USER.get();
// lógica de negócio usando user
}Funciona, mas tem duas fragilidades conhecidas: se alguém esquecer o remove() no finally, o valor vaza para a próxima requisição atendida pela mesma thread (grave em pool de threads reutilizadas); e nada impede que uma camada mais profunda chame CURRENT_USER.set() de novo, mutando o contexto no meio do processamento de forma silenciosa.
Código depois: Scoped Values
A mesma lógica, reescrita com a API que o JEP 506 tornou definitiva:
private static final ScopedValue<UserContext> CURRENT_USER = ScopedValue.newInstance();
public void handleRequest(Request req) {
ScopedValue.where(CURRENT_USER, loadUser(req))
.run(() -> processRequest(req));
}
public void processRequest(Request req) {
UserContext user = CURRENT_USER.get();
// lógica de negócio usando user
}O ganho não é só estético. O runtime sabe, em tempo de compilação e execução, que CURRENT_USER só existe dentro do run(). Não há remove() para esquecer porque não há estado para limpar: quando o run() termina, o binding desaparece junto. E se uma thread filha for criada dentro daquele escopo (por exemplo, via StructuredTaskScope, que é justamente o outro JEP desta LTS), ela herda o mesmo valor sem custo de cópia.
O trade-off que a imutabilidade impõe
A contrapartida é dura para código legado: qualquer trecho que dependia de mudar o ThreadLocal no meio do caminho (padrão comum em pipelines de autenticação que enriquecem o contexto em etapas) precisa ser reestruturado para que todo o valor final já esteja disponível antes de entrar no escopo where(). Isso é atrito real de migração, especialmente em bases de código com anos de camadas de middleware que fazem set() sucessivos. Um caminho razoável para migrar de forma gradual num serviço já em produção: manter a API de leitura (get()) compatível numa fachada própria, e trocar a implementação por baixo de ThreadLocal para ScopedValue seção por seção, começando pelos módulos onde o contexto é definido uma única vez no início da requisição \u2014 o caso mais comum e o de menor risco.
Structured Concurrency: ainda preview, e isso importa
O JEP 505 chega ao JDK 25 na quinta rodada de preview, o que por si só é um sinal: a API de StructuredTaskScope ainda está sendo ajustada release a release desde que apareceu como incubator. Isso muda a recomendação prática: para experimentar em serviço de produção, a chamada exige --enable-preview na JVM, o que automaticamente restringe onde e como usar \u2014 nenhuma equipe deveria colocar --enable-preview num deploy que atende tráfego real sem um plano de rollback claro, porque uma API preview pode mudar assinatura na LTS seguinte sem aviso de compatibilidade.
A ideia do Structured Concurrency é tratar um grupo de tarefas concorrentes como uma unidade só, com ciclo de vida amarrado: se uma falha, as outras são canceladas; se o escopo é fechado, nenhuma tarefa filha sobrevive além dele. Um esboço do padrão, combinando com Scoped Values para propagar contexto às subtarefas:
try (var scope = StructuredTaskScope.open()) {
var user = scope.fork(() -> fetchUser(id));
var orders = scope.fork(() -> fetchOrders(id));
scope.join();
return new Response(user.get(), orders.get());
}O valor prático aqui é eliminar uma classe de bug clássica de ExecutorService manual: task órfã que continua rodando depois que o método que a criou já retornou, consumindo CPU e conexão sem que ninguém saiba. Num projeto típico, o caminho recomendado seria isolar esse código em um módulo experimental, atrás de feature flag, testado sob carga em staging antes de qualquer cogitação de produção \u2014 e só migrar de fato quando a API sair de preview, o que a numeração (quinta rodada) sugere que ainda vai levar pelo menos mais uma ou duas LTS.
O que fica de fora desse guia
Esta LTS também formaliza o JEP 511 (Module Import Declarations) e o JEP 512 (Compact Source Files), que mudam a experiência de escrita de código Java simples (scripts, protótipos), mas não afetam quem já tem serviço rodando com módulos e build consolidado. Vale acompanhar, mas não competem em urgência com a troca de ThreadLocal por Scoped Values, que é a mudança de comportamento mais concreta desta release para código de produção existente.
A recomendação prática para quem mantém serviço Java hoje: subir para JDK 25 pelo suporte LTS já vale por si (compatibilidade, patches de segurança de longo prazo), a migração de ThreadLocal para Scoped Values pode e deve começar já, módulo por módulo, começando pelos contextos que não sofrem mutação incremental; e Structured Concurrency fica reservado para experimentação isolada, sem --enable-preview em caminho crítico, até a API estabilizar.
Fonte 1: OpenJDK — JDK 25 Release Notes e JEP Index (https://openjdk.org/projects/jdk/25/)
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.















