ScopedValue: O Sucessor Moderno do ThreadLocal no Java 21+
Com a chegada das Virtual Threads no Java 21, a escalabilidade atingiu um novo patamar, permitindo que aplicações gerenciem milhões de threads simultaneamente. No entanto, essa nova densidade expôs fraquezas em ferramentas veteranas, como o ThreadLocal. Para resolver esses problemas, o Java introduziu o ScopedValue (finalizado no Java 25 via JEP 506), uma funcionalidade fundamental para a concorrência moderna.
Neste artigo, vamos explorar por que o ThreadLocal se tornou um gargalo, como o ScopedValue oferece uma alternativa mais segura e eficiente, e como ele se integra ao ecossistema de Virtual Threads e Structured Concurrency.
📦 Repositório de referência: github.com/guigomes91/virtual-threads
O Problema: As Deficiências do ThreadLocal
O ThreadLocal permite que dados sejam armazenados localmente em uma thread, mas possui três falhas críticas de design que se agravam com Virtual Threads:
- Mutabilidade Descontrolada: Qualquer componente com acesso à variável pode chamar
set(), tornando o rastreio de alterações e o debug extremamente complexos. - Vazamentos de Memória (Memory Leaks): Os dados persistem enquanto a thread estiver viva, a menos que
remove()seja chamado explicitamente. Em ambientes de longa duração, esquecer de limpar essas variáveis causa vazamentos graves. - Herança Cara: Quando uma thread pai cria threads filhas, todas as variáveis
ThreadLocalsão copiadas para as novas threads. Com milhões de Virtual Threads, essa duplicação de dados esgota rapidamente o heap da JVM.
Exemplo do problema com ThreadLocal
<span class="hljs-comment">// ⚠️ Padrão problemático com ThreadLocal</span>
<span class="hljs-keyword">public</span> <span class="hljs-keyword">class</span> <span class="hljs-title class_">RequestContext</span> {
<span class="hljs-keyword">private</span> <span class="hljs-keyword">static</span> <span class="hljs-keyword">final</span> ThreadLocal<String> USER_ID = <span class="hljs-keyword">new</span> <span class="hljs-title class_">ThreadLocal</span><>();
<span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title function_">handleRequest</span><span class="hljs-params">(String userId)</span> {
USER_ID.set(userId);
<span class="hljs-keyword">try</span> {
processRequest();
} <span class="hljs-keyword">finally</span> {
USER_ID.remove(); <span class="hljs-comment">// esqueceu isso? -> memory leak</span>
}
}
<span class="hljs-keyword">private</span> <span class="hljs-keyword">void</span> <span class="hljs-title function_">processRequest</span><span class="hljs-params">()</span> {
<span class="hljs-comment">// Qualquer código pode chamar set() e corromper o estado</span>
USER_ID.set(<span class="hljs-string">"hacker"</span>); <span class="hljs-comment">// ⚠️ mutação silenciosa e perigosa</span>
System.out.println(<span class="hljs-string">"Processando para: "</span> + USER_ID.get());
}
}
A Solução: ScopedValue
O ScopedValue surge como uma alternativa de "via de mão única" para compartilhar dados imutáveis entre componentes e suas sub-tarefas de forma segura.
Os Pilares do ScopedValue
| Característica | ThreadLocal | ScopedValue |
|---|---|---|
| Mutabilidade | set() livre |
Imutável após where() |
| Tempo de vida | Lifetime da thread | Escopo da tarefa |
| Herança | Cópia completa (cara) | Referência compartilhada (grátis) |
| Compatibilidade | Pool de threads | Virtual Threads nativas |
| Risco de leak | Alto | Nenhum |
- Imutabilidade: Não existe o método
set(). Uma vez vinculado viawhere(), o valor permanece constante durante todo o escopo de execução. - Tempo de Vida Delimitado: O valor é vinculado apenas à execução de um método específico. Assim que a tarefa termina, o valor é invalidado automaticamente, eliminando o risco de memory leaks.
- Eficiência de Memória: Threads filhas (especialmente dentro de um
StructuredTaskScope) compartilham a mesma referência do valor da thread pai, sem cópias custosas.
Como Implementar na Prática
1. Declaração
Geralmente declarado como campo estático e final, com acesso restrito para garantir encapsulamento.
<span class="hljs-keyword">public</span> <span class="hljs-keyword">class</span> <span class="hljs-title class_">SecurityContext</span> {
<span class="hljs-comment">// private: apenas esta classe pode fazer bind e leitura</span>
<span class="hljs-keyword">private</span> <span class="hljs-keyword">static</span> <span class="hljs-keyword">final</span> ScopedValue<String> USER_ID = ScopedValue.newInstance();
<span class="hljs-comment">// public: expõe acesso de leitura para outros componentes de forma controlada</span>
<span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> String <span class="hljs-title function_">currentUserId</span><span class="hljs-params">()</span> {
<span class="hljs-keyword">return</span> USER_ID.get();
}
}
2. Vinculação e Execução (Binding)
A vinculação define onde e por quanto tempo o valor estará disponível. É possível encadear múltiplos valores.
<span class="hljs-comment">// Vinculando um único valor</span>
ScopedValue.where(USER_ID, <span class="hljs-string">"getcaramelo"</span>)
.run(() -> processarRequisicao());
<span class="hljs-comment">// Encadeando múltiplos valores</span>
ScopedValue.where(USER_ID, <span class="hljs-string">"getcaramelo"</span>)
.where(REQUEST_ID, UUID.randomUUID().toString())
.where(TENANT_ID, <span class="hljs-string">"acme-corp"</span>)
.run(() -> processarRequisicao());
3. Recuperação Segura
<span class="hljs-comment">// Verificação antes do acesso</span>
<span class="hljs-keyword">if</span> (USER_ID.isBound()) {
System.out.println(<span class="hljs-string">"Usuário atual: "</span> + USER_ID.get());
}
<span class="hljs-comment">// Valor padrão caso não esteja vinculado (Java 25: orElse não aceita null)</span>
<span class="hljs-type">String</span> <span class="hljs-variable">userId</span> <span class="hljs-operator">=</span> USER_ID.orElse(<span class="hljs-string">"anonimo"</span>);
<span class="hljs-comment">// Lança exceção se não estiver vinculado</span>
<span class="hljs-keyword">try</span> {
<span class="hljs-type">String</span> <span class="hljs-variable">id</span> <span class="hljs-operator">=</span> USER_ID.get();
} <span class="hljs-keyword">catch</span> (NoSuchElementException e) {
<span class="hljs-comment">// ScopedValue não foi vinculado neste escopo</span>
}
4. Retornando valores com call()
Além de run() (que retorna void), use call() quando precisar de um resultado:
<span class="hljs-comment">// run() -> void</span>
ScopedValue.where(USER_ID, <span class="hljs-string">"getcaramelo"</span>)
.run(() -> salvarAuditoria());
<span class="hljs-comment">// call() -> retorna valor</span>
<span class="hljs-type">String</span> <span class="hljs-variable">resultado</span> <span class="hljs-operator">=</span> ScopedValue.where(USER_ID, <span class="hljs-string">"getcaramelo"</span>)
.call(() -> buscarPerfil());
ScopedValue em Fluxos Reais
Exemplo 1: TravelApp com Structured Concurrency
No cenário de um sistema de viagens, o ScopedValue organiza o fluxo de dados entre microserviços concorrentes de forma limpa, sem passar parâmetros por toda a pilha de chamadas.
<span class="hljs-keyword">public</span> <span class="hljs-keyword">class</span> <span class="hljs-title class_">TravelService</span> {
<span class="hljs-keyword">private</span> <span class="hljs-keyword">static</span> <span class="hljs-keyword">final</span> ScopedValue<String> LOC = ScopedValue.newInstance();
<span class="hljs-keyword">private</span> <span class="hljs-keyword">static</span> <span class="hljs-keyword">final</span> ScopedValue<String> DEST = ScopedValue.newInstance();
<span class="hljs-keyword">public</span> TravelOffer <span class="hljs-title function_">fetchTravelOffers</span><span class="hljs-params">(String origem, String destino)</span> <span class="hljs-keyword">throws</span> Exception {
<span class="hljs-keyword">return</span> ScopedValue.where(LOC, origem)
.where(DEST, destino)
.call(() -> {
<span class="hljs-keyword">try</span> (<span class="hljs-type">var</span> <span class="hljs-variable">scope</span> <span class="hljs-operator">=</span> <span class="hljs-keyword">new</span> <span class="hljs-title class_">StructuredTaskScope</span>.ShutdownOnFailure()) {
<span class="hljs-comment">// As sub-tarefas herdam LOC e DEST automaticamente</span>
<span class="hljs-comment">// sem nenhuma cópia de memória</span>
<span class="hljs-type">var</span> <span class="hljs-variable">ridesharing</span> <span class="hljs-operator">=</span> scope.fork(<span class="hljs-built_in">this</span>::fetchRidesharing);
<span class="hljs-type">var</span> <span class="hljs-variable">publicTransport</span> <span class="hljs-operator">=</span> scope.fork(<span class="hljs-built_in">this</span>::fetchPublicTransport);
<span class="hljs-type">var</span> <span class="hljs-variable">taxi</span> <span class="hljs-operator">=</span> scope.fork(<span class="hljs-built_in">this</span>::fetchTaxi);
scope.join().throwIfFailed();
<span class="hljs-keyword">return</span> buildOffer(
ridesharing.get(),
publicTransport.get(),
taxi.get()
);
}
});
}
<span class="hljs-keyword">private</span> RidesharingQuote <span class="hljs-title function_">fetchRidesharing</span><span class="hljs-params">()</span> {
<span class="hljs-comment">// LOC e DEST disponíveis sem parâmetros!</span>
<span class="hljs-type">String</span> <span class="hljs-variable">from</span> <span class="hljs-operator">=</span> LOC.get();
<span class="hljs-type">String</span> <span class="hljs-variable">to</span> <span class="hljs-operator">=</span> DEST.get();
<span class="hljs-keyword">return</span> ridesharingApi.quote(from, to);
}
<span class="hljs-keyword">private</span> TransportQuote <span class="hljs-title function_">fetchPublicTransport</span><span class="hljs-params">()</span> {
<span class="hljs-keyword">return</span> transitApi.routes(LOC.get(), DEST.get());
}
}
Exemplo 2: Segurança em APIs Web (contexto de autenticação)
Padrão comum em frameworks web para propagar o usuário autenticado sem injeção de dependência manual.
<span class="hljs-keyword">public</span> <span class="hljs-keyword">class</span> <span class="hljs-title class_">SecurityFilter</span> {
<span class="hljs-keyword">static</span> <span class="hljs-keyword">final</span> ScopedValue<Principal> PRINCIPAL = ScopedValue.newInstance();
<span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title function_">doFilter</span><span class="hljs-params">(HttpRequest request, FilterChain chain)</span> {
<span class="hljs-type">Principal</span> <span class="hljs-variable">principal</span> <span class="hljs-operator">=</span> authenticate(request);
ScopedValue.where(PRINCIPAL, principal)
.run(() -> chain.doFilter(request));
<span class="hljs-comment">// Após o run(), PRINCIPAL é automaticamente invalidado</span>
}
}
<span class="hljs-keyword">public</span> <span class="hljs-keyword">class</span> <span class="hljs-title class_">OrderService</span> {
<span class="hljs-keyword">public</span> Order <span class="hljs-title function_">createOrder</span><span class="hljs-params">(OrderRequest req)</span> {
<span class="hljs-comment">// Acessa o principal sem receber por parâmetro</span>
<span class="hljs-type">Principal</span> <span class="hljs-variable">user</span> <span class="hljs-operator">=</span> SecurityFilter.PRINCIPAL.get();
<span class="hljs-keyword">if</span> (!user.hasRole(<span class="hljs-string">"CUSTOMER"</span>)) {
<span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> <span class="hljs-title class_">AccessDeniedException</span>(<span class="hljs-string">"Acesso negado para: "</span> + user.getName());
}
<span class="hljs-keyword">return</span> orderRepository.save(<span class="hljs-keyword">new</span> <span class="hljs-title class_">Order</span>(req, user.getName()));
}
}
Exemplo 3: Rastreamento distribuído (Tracing)
Propagação de traceId para logs estruturados em sistemas distribuídos.
<span class="hljs-keyword">public</span> <span class="hljs-keyword">class</span> <span class="hljs-title class_">TracingContext</span> {
<span class="hljs-keyword">private</span> <span class="hljs-keyword">static</span> <span class="hljs-keyword">final</span> ScopedValue<String> TRACE_ID = ScopedValue.newInstance();
<span class="hljs-keyword">private</span> <span class="hljs-keyword">static</span> <span class="hljs-keyword">final</span> ScopedValue<String> SPAN_ID = ScopedValue.newInstance();
<span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> <span class="hljs-keyword">void</span> <span class="hljs-title function_">withNewTrace</span><span class="hljs-params">(Runnable task)</span> {
ScopedValue.where(TRACE_ID, UUID.randomUUID().toString())
.where(SPAN_ID, UUID.randomUUID().toString())
.run(task);
}
<span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> String <span class="hljs-title function_">currentTraceId</span><span class="hljs-params">()</span> {
<span class="hljs-keyword">return</span> TRACE_ID.orElse(<span class="hljs-string">"sem-trace"</span>);
}
<span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> String <span class="hljs-title function_">currentSpanId</span><span class="hljs-params">()</span> {
<span class="hljs-keyword">return</span> SPAN_ID.orElse(<span class="hljs-string">"sem-span"</span>);
}
}
<span class="hljs-comment">// Uso em qualquer camada da aplicação</span>
<span class="hljs-keyword">public</span> <span class="hljs-keyword">class</span> <span class="hljs-title class_">PaymentService</span> {
<span class="hljs-keyword">private</span> <span class="hljs-keyword">static</span> <span class="hljs-keyword">final</span> <span class="hljs-type">Logger</span> <span class="hljs-variable">log</span> <span class="hljs-operator">=</span> LoggerFactory.getLogger(PaymentService.class);
<span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title function_">processPayment</span><span class="hljs-params">(Payment payment)</span> {
<span class="hljs-comment">// Log automaticamente enriquecido com o trace</span>
log.info(<span class="hljs-string">"[traceId={}] [spanId={}] Processando pagamento de {}"</span>,
TracingContext.currentTraceId(),
TracingContext.currentSpanId(),
payment.amount());
}
}
Exemplo 4: Rebinding - Sobrescrita Controlada de Escopo
Em alguns cenários, uma chamada aninhada precisa de um valor diferente no mesmo ScopedValue. O rebinding cria um novo escopo sem afetar o escopo externo.
<span class="hljs-keyword">static</span> <span class="hljs-keyword">final</span> ScopedValue<String> ROLE = ScopedValue.newInstance();
<span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title function_">demonstrate</span><span class="hljs-params">()</span> {
ScopedValue.where(ROLE, <span class="hljs-string">"USER"</span>).run(() -> {
System.out.println(<span class="hljs-string">"Escopo externo: "</span> + ROLE.get()); <span class="hljs-comment">// "USER"</span>
<span class="hljs-comment">// Rebinding: cria um escopo interno com valor diferente</span>
ScopedValue.where(ROLE, <span class="hljs-string">"ADMIN"</span>).run(() -> {
System.out.println(<span class="hljs-string">"Escopo interno: "</span> + ROLE.get()); <span class="hljs-comment">// "ADMIN"</span>
});
<span class="hljs-comment">// Escopo externo é restaurado automaticamente</span>
System.out.println(<span class="hljs-string">"Escopo externo restaurado: "</span> + ROLE.get()); <span class="hljs-comment">// "USER"</span>
});
}
Conexão com Virtual Threads: Por Que Isso Importa
O repositório guigomes91/virtual-threads demonstra como evitar thread pinning usando ReentrantLock em vez de synchronized. O ScopedValue é o par natural dessa estratégia: enquanto o ReentrantLock libera a carrier thread durante bloqueios, o ScopedValue garante que o contexto seja propagado corretamente para os milhões de Virtual Threads sem explosão de memória.
<span class="hljs-comment">// ✅ Padrão completo: Virtual Thread + ReentrantLock + ScopedValue</span>
<span class="hljs-keyword">public</span> <span class="hljs-keyword">class</span> <span class="hljs-title class_">ModernRequestHandler</span> {
<span class="hljs-keyword">private</span> <span class="hljs-keyword">static</span> <span class="hljs-keyword">final</span> ScopedValue<RequestContext> CTX = ScopedValue.newInstance();
<span class="hljs-keyword">private</span> <span class="hljs-keyword">final</span> <span class="hljs-type">Lock</span> <span class="hljs-variable">lock</span> <span class="hljs-operator">=</span> <span class="hljs-keyword">new</span> <span class="hljs-title class_">ReentrantLock</span>();
<span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title function_">handle</span><span class="hljs-params">(HttpRequest request)</span> {
<span class="hljs-type">var</span> <span class="hljs-variable">context</span> <span class="hljs-operator">=</span> RequestContext.from(request);
<span class="hljs-comment">// 1. Propaga contexto imutável via ScopedValue</span>
ScopedValue.where(CTX, context).run(() -> {
lock.lock(); <span class="hljs-comment">// 2. ReentrantLock evita pinning da Virtual Thread</span>
<span class="hljs-keyword">try</span> {
processWithContext(); <span class="hljs-comment">// carrier thread é liberada durante I/O</span>
} <span class="hljs-keyword">finally</span> {
lock.unlock();
}
});
<span class="hljs-comment">// 3. Contexto invalidado automaticamente após o run()</span>
}
<span class="hljs-keyword">private</span> <span class="hljs-keyword">void</span> <span class="hljs-title function_">processWithContext</span><span class="hljs-params">()</span> {
<span class="hljs-type">RequestContext</span> <span class="hljs-variable">ctx</span> <span class="hljs-operator">=</span> CTX.get();
<span class="hljs-comment">// ... lógica de negócio usando o contexto</span>
}
}
Histórico de Evolução e Status no Java 25
| Java | JEP | Status |
|---|---|---|
| 20 | 429 | Incubator |
| 21 | 446 | 1º Preview |
| 22 | 464 | 2º Preview |
| 23 | 481 | 3º Preview |
| 24 | 487 | 4º Preview |
| 25 (LTS) | 506 | ✅ Finalizado |
A única mudança na finalização: ScopedValue.orElse() não aceita mais null como argumento. A API chegou estável, sem flags de preview.
Quando NÃO usar ScopedValue
ScopedValue não é um substituto direto para todo caso de ThreadLocal. Há cenários em que ThreadLocal ainda é apropriado:
- Estado mutável por design: Se você precisa acumular dados ao longo de uma thread (ex.: contadores, listas que crescem),
ThreadLocalainda faz sentido. - Integração com código legado: Frameworks como Spring e Hibernate usam
ThreadLocalinternamente. A migração deve ser gradual. - Escopos verdadeiramente abertos: Se o valor precisa sobreviver além do escopo da chamada de método (ex.: dados mantidos entre requisições na mesma thread de um pool),
ScopedValuenão é a ferramenta certa.
Conclusão
O ScopedValue não é apenas uma melhoria incremental, é um componente fundamental para a arquitetura de sistemas modernos em Java. Ao forçar a imutabilidade e restringir o tempo de vida dos dados ao escopo da tarefa, ele oferece a segurança e a leveza necessárias para que as Virtual Threads alcancem seu potencial máximo de vazão sem comprometer a estabilidade da memória.
Com o Java 25 (LTS) finalizando a API, não há mais razão para usar --enable-preview. É hora de considerar o ScopedValue como o padrão para propagação de contexto em qualquer código novo.
Referências
- JEP 506: Scoped Values (Final)
- JEP 444: Virtual Threads
- JEP 505: Structured Concurrency (Final)
- Repositório: guigomes91/virtual-threads
- Java Coding Problems, Second Edition — Anghel Leonard
Publicado por: Guilherme Gomes - 24/05/2026 17:27
Caramelo.dev