devto 2026-07-30 원문 보기 ↗
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? 🚀
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();
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.
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.
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.
O comportamento do ThreadLocal depende do contexto de execução da thread, o que torna os testes imprevisíveis e difíceis de controlar.
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.
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.
O Java 21 trouxe os Scoped Values (JEP 446). A ideia é simples e elegante:
ThreadLocal), o ScopedValue prende o valor a um bloco de código.remove(), sem memory leak.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);
}
}
O que acontece aqui?
userId fica disponível apenas dentro do bloco run() e em todos os métodos chamados a partir dele.remove() o Java cuida de tudo.| 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 |
userId, transactionId, tenantId) por uma cadeia de chamadasremove() e memory leakSe 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.
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! 👇
Quer mais? No próximo post vou mostrar como migrar um código real de ThreadLocal para ScopedValue num projeto Spring Boot. Até lá! 🚀