E aí, dev que já passou horas debugando um ThreadLocal que não foi limpo e vazou memória? Pois é. Esse velho conhecido do Java 1.2 sempre foi a solução para guardar dados "por thread" como o userId de uma requisição, ou uma conexão de banco que não pode ser compartilhada.

Mas aí chegaram as Virtual Threads no JDK 21, e o jogo mudou. Criar milhões de threads leves virou realidade, e o bom e velho ThreadLocal começou a mostrar suas fraquezas. A boa notícia? O Java 21 trouxe uma alternativa moderna e elegante: Scoped Values.

Vamos entender tudo isso com exemplos práticos. Bora? 🚀


🔍 O que é ThreadLocal (e por que você usava isso)?

ThreadLocal é uma classe que permite criar variáveis que só podem ser acessadas e modificadas pela mesma thread. Cada thread que acessa um ThreadLocal tem sua própria cópia independente da variável.

Exemplo clássico:

public class ContextoUsuario {
    private static final ThreadLocal<String> USER_ID = new ThreadLocal<>();

    public static void setUserId(String userId) {
        USER_ID.set(userId);
    }

    public static String getUserId() {
        return USER_ID.get();
    }

    public static void clear() {
        USER_ID.remove(); // 👈 NUNCA ESQUEÇA DISSO!
    }
}

// Em algum lugar do código...
ContextoUsuario.setUserId("alice123");
String user = ContextoUsuario.getUserId(); // "alice123"
ContextoUsuario.clear();

Enter fullscreen mode Exit fullscreen mode

Use quando: precisar associar dados a uma thread específica sem passar parâmetros por toda a cadeia de chamadas tipo userId numa requisição web, conexão de banco, ou transação.


⚠️ Os problemas clássicos do ThreadLocal (que todo mundo já sofreu)

1. 💀 Memory Leak

Se você esquecer de chamar remove(), o valor fica preso na thread. Se a thread for reaproveitada (como num pool), o valor vaza e pode nunca ser coletado.

2. 🔄 Contaminação entre requisições

Num pool de threads, uma requisição pode "herdar" o ThreadLocal da requisição anterior, porque a thread foi reaproveitada. Resultado: usuário A vê dados do usuário B. Um pesadelo de segurança.

3. 🧪 Dificuldade de testar

O comportamento do ThreadLocal depende do contexto de execução da thread, o que torna os testes imprevisíveis e difíceis de controlar.

4. 🌍 Global state disfarçado

Embora sejam "locais à thread", múltiplas threads compartilham o mesmo código. O uso excessivo cria dependências escondidas que tornam o código difícil de entender.


🧵 O problema com Virtual Threads

Aqui a coisa fica séria.

Virtual threads são leves e descartáveis você pode criar milhões delas. Elas aparecem e desaparecem em frações de milissegundo.

O ThreadLocal, por sua vez, foi feito para threads longas e reutilizáveis. Com virtual threads, cada uma pode ter seu próprio ThreadLocal e, se você estiver armazenando objetos grandes, o heap pode explodir com milhões de cópias.

Além disso, se a virtual thread terminar e o ThreadLocal não for limpo, o valor pode continuar ocupando memória mesmo depois que a thread já morreu.

⚠️ Importante: Nas versões preview do JDK 19 e 20, era possível criar virtual threads sem suporte a ThreadLocal. No JDK 21, todas as virtual threads suportam ThreadLocal para garantir compatibilidade com bibliotecas existentes. Mas isso não significa que você deva usar significa que você precisa ter mais cuidado ainda.


✨ A novidade do Java 21: Scoped Values

O Java 21 trouxe os Scoped Values (JEP 446). A ideia é simples e elegante:

  • Em vez de guardar dados dentro da thread (como o ThreadLocal), o ScopedValue prende o valor a um bloco de código.
  • Quando o bloco termina, o valor some automaticamente sem remove(), sem memory leak.
  • É imutável: uma vez bindado, não pode ser alterado.
  • Funciona perfeitamente com virtual threads.

Exemplo com ScopedValue:

public class ContextoModerno {
    // Declara o ScopedValue (geralmente static final)
    private static final ScopedValue<String> USER_ID = ScopedValue.newInstance();

    public void processarRequisicao(String userId) {
        // "Amarra" o valor a um bloco de código
        ScopedValue.where(USER_ID, userId).run(() -> {
            // Dentro deste bloco, USER_ID.get() retorna "userId"
            System.out.println("Usuário: " + USER_ID.get());
            chamarOutroMetodo(); // O valor continua disponível
        });
        // Fora do bloco, USER_ID.get() lança exceção
    }

    private void chamarOutroMetodo() {
        // Ainda consegue acessar o valor aqui dentro
        String user = USER_ID.get();
        System.out.println("Método aninhado: " + user);
    }
}

Enter fullscreen mode Exit fullscreen mode

O que acontece aqui?

  • O valor userId fica disponível apenas dentro do bloco run() e em todos os métodos chamados a partir dele.
  • Quando o bloco termina, o valor é automaticamente descartado.
  • Nada de remove() o Java cuida de tudo.

📊 Comparativo rápido

Característica ThreadLocal ScopedValue (Java 21)
Quando surgiu Java 1.2 Java 21
Onde os dados ficam Dentro da thread (ThreadLocalMap) Vinculados ao bloco de código (stack)
Limpeza Manual (remove()) Automática ao sair do bloco
Imutável ❌ Pode ser alterado Imutável após bind
Virtual threads Funciona, mas com risco de memory leak Projetado para virtual threads
Performance Busca em hashtable por thread Mais eficiente em cenários de curta duração
Risco Memory leak, contaminação entre threads Mínimo

🧭 Quando usar cada um?

✅ Use ThreadLocal se:

  • Você está mantendo código legado que já usa ThreadLocal
  • Precisa de um valor que realmente deve viver durante toda a vida da thread (ex: pool de conexões JDBC)
  • Não está usando virtual threads (ou está usando platform threads tradicionais)

✅ Use ScopedValue se:

  • Está no Java 21 ou superior
  • Usa virtual threads (ou planeja usar)
  • Precisa passar contexto (ex: userId, transactionId, tenantId) por uma cadeia de chamadas
  • Quer código mais seguro, limpo e sem memory leak

❌ Evite ThreadLocal se:

  • Está usando virtual threads e armazenando objetos grandes
  • Não quer se preocupar com remove() e memory leak
  • Quer código mais fácil de testar e entender

💡 Dica de ouro

Se você está no Java 21 e usando virtual threads, ScopedValue é o caminho. ThreadLocal ainda funciona, mas você vai pagar o preço em memória e complexidade.

E lembre-se: se for usar ThreadLocal em qualquer cenário, sempre envolva o uso com try-finally e chame remove() no finally. É feio, mas é necessário.


🎯 Conclusão

O ThreadLocal serviu bem por décadas, mas o mundo mudou. As virtual threads vieram para ficar, e com elas veio a necessidade de ferramentas mais modernas e seguras.

O ScopedValue não é só "um ThreadLocal melhor" é uma mudança de paradigma: dados atrelados a blocos de código, não a threads. Mais seguro, mais performático, mais fácil de raciocinar.

Da próxima vez que for guardar um userId ou um contexto de requisição, pergunte-se: estou no Java 21? Se sim, dê uma chance ao ScopedValue. Sua memória (e seu eu do futuro debugando) vão agradecer. 🧵✨

Já usou ScopedValue? Ou ainda sofre com ThreadLocal? Conta aí nos comentários! 👇

Java21 #VirtualThreads #ThreadLocal #ScopedValue #Concorrência


Quer mais? No próximo post vou mostrar como migrar um código real de ThreadLocal para ScopedValue num projeto Spring Boot. Até lá! 🚀