Java9 min de leitura

ScopedValues sucessor do ThreadLocal

ScopedValue: O Sucessor Moderno do ThreadLocal no Java 21+

ScopedValues

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:

  1. Mutabilidade Descontrolada: Qualquer componente com acesso à variável pode chamar set(), tornando o rastreio de alterações e o debug extremamente complexos.
  2. 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.
  3. Herança Cara: Quando uma thread pai cria threads filhas, todas as variáveis ThreadLocal sã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&lt;String&gt; USER_ID = <span class="hljs-keyword">new</span> <span class="hljs-title class_">ThreadLocal</span>&lt;&gt;();

    <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? -&gt; 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">&quot;hacker&quot;</span>); <span class="hljs-comment">// ⚠️ mutação silenciosa e perigosa</span>
        System.out.println(<span class="hljs-string">&quot;Processando para: &quot;</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 via where(), 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&lt;String&gt; 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">&quot;getcaramelo&quot;</span>)
           .run(() -&gt; processarRequisicao());

<span class="hljs-comment">// Encadeando múltiplos valores</span>
ScopedValue.where(USER_ID, <span class="hljs-string">&quot;getcaramelo&quot;</span>)
           .where(REQUEST_ID, UUID.randomUUID().toString())
           .where(TENANT_ID, <span class="hljs-string">&quot;acme-corp&quot;</span>)
           .run(() -&gt; 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">&quot;Usuário atual: &quot;</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">&quot;anonimo&quot;</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() -&gt; void</span>
ScopedValue.where(USER_ID, <span class="hljs-string">&quot;getcaramelo&quot;</span>)
           .run(() -&gt; salvarAuditoria());

<span class="hljs-comment">// call() -&gt; 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">&quot;getcaramelo&quot;</span>)
                              .call(() -&gt; 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&lt;String&gt; LOC  = ScopedValue.newInstance();
    <span class="hljs-keyword">private</span> <span class="hljs-keyword">static</span> <span class="hljs-keyword">final</span> ScopedValue&lt;String&gt; 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(() -&gt; {
                              <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&lt;Principal&gt; 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(() -&gt; 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">&quot;CUSTOMER&quot;</span>)) {
            <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> <span class="hljs-title class_">AccessDeniedException</span>(<span class="hljs-string">&quot;Acesso negado para: &quot;</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&lt;String&gt; TRACE_ID  = ScopedValue.newInstance();
    <span class="hljs-keyword">private</span> <span class="hljs-keyword">static</span> <span class="hljs-keyword">final</span> ScopedValue&lt;String&gt; 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">&quot;sem-trace&quot;</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">&quot;sem-span&quot;</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">&quot;[traceId={}] [spanId={}] Processando pagamento de {}&quot;</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&lt;String&gt; 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">&quot;USER&quot;</span>).run(() -&gt; {
        System.out.println(<span class="hljs-string">&quot;Escopo externo: &quot;</span> + ROLE.get()); <span class="hljs-comment">// &quot;USER&quot;</span>

        <span class="hljs-comment">// Rebinding: cria um escopo interno com valor diferente</span>
        ScopedValue.where(ROLE, <span class="hljs-string">&quot;ADMIN&quot;</span>).run(() -&gt; {
            System.out.println(<span class="hljs-string">&quot;Escopo interno: &quot;</span> + ROLE.get()); <span class="hljs-comment">// &quot;ADMIN&quot;</span>
        });

        <span class="hljs-comment">// Escopo externo é restaurado automaticamente</span>
        System.out.println(<span class="hljs-string">&quot;Escopo externo restaurado: &quot;</span> + ROLE.get()); <span class="hljs-comment">// &quot;USER&quot;</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&lt;RequestContext&gt; 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(() -&gt; {
            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), ThreadLocal ainda faz sentido.
  • Integração com código legado: Frameworks como Spring e Hibernate usam ThreadLocal internamente. 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), ScopedValue nã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


Publicado por: Guilherme Gomes - 24/05/2026 17:27

← Voltar aos Artigos