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), oScopedValueprende 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
userIdfica disponível apenas dentro do blocorun()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á! 🚀
0 Comments
Log in to join the conversation.No comments yet. Be the first to share your thoughts.