1. Por que o ThreadLocal está perdendo relevância
Para que serve o ThreadLocal, afinal?
Na multithread clássica, em que as threads vivem por muito tempo (por exemplo, em um servidor), às vezes é preciso armazenar dados próprios para cada thread — dados que não devem se misturar com os de outras. Por exemplo, o nome do usuário, o ID da requisição ou um buffer temporário.
Para isso, o Java introduziu ThreadLocal<T> — uma espécie de “espaço pessoal” da thread, onde é possível armazenar dados sem atrapalhar os vizinhos:
ThreadLocal<String> user = new ThreadLocal<>();
user.set("Alice"); // o valor é armazenado apenas para esta thread
String name = user.get(); // retornará "Alice" aqui; em outras threads — null
Por que o ThreadLocal não se dá bem com threads virtuais
As threads virtuais vivem de um jeito bem diferente das antigas threads “pesadas”. Elas aparecem e desaparecem aos milhares — às vezes em frações de milissegundo. E o ThreadLocal vincula dados a uma thread específica, como se ela fosse viver para sempre.
Quando uma thread virtual termina o trabalho, seus dados no ThreadLocal podem continuar pendurados na memória — mesmo que a própria thread já tenha morrido há muito tempo. Isso leva a vazamentos, pois a JVM nem sempre sabe que esses valores não são mais necessários.
E se as threads são reutilizadas (por exemplo, em pools), surge uma situação ainda mais desagradável: o “contexto de outra pessoa” pode, por acaso, ir parar em uma nova requisição. Imagine: o usuário Petya recebe os dados de Vasya — e pronto, bugs e vulnerabilidades.
O ThreadLocal funciona muito bem onde há poucas threads e elas vivem por muito tempo. Mas com threads virtuais — é como tentar guardar coisas em um armário que desaparece a cada segundo.
2. Scoped Values: uma nova forma de propagar contexto
Scoped Values é uma ferramenta recente do Java 21 que resolve o velho problema do ThreadLocal, mas de forma elegante. Em vez de armazenar dados dentro da thread, como no ThreadLocal, ele “fixa” os dados ao escopo de execução — ou seja, a um trecho específico de código. O valor vive apenas enquanto esse trecho é executado e depois desaparece automaticamente, sem deixar rastros na memória.
import java.lang.ScopedValue;
ScopedValue<String> USER = ScopedValue.newInstance();
ScopedValue.where(USER, "Alice").run(() -> {
System.out.println("Hello, " + USER.get()); // Irá imprimir: Hello, Alice
});
Quando o código sai do bloco run, o valor já não está disponível — tentar acessá-lo lançará uma exceção. Não é preciso limpar nada manualmente.
Scoped Values não poluem a memória, não confundem o contexto entre threads e permitem criar escopos aninhados, em que valores internos sobrepõem temporariamente os externos. É uma forma organizada, previsível e segura de propagar contexto, especialmente no mundo das threads virtuais.
3. Exemplos de uso de Scoped Values
Exemplo 1: Propagação do contexto do usuário
Suponha que temos um servidor que processa requisições de diferentes usuários. Para cada requisição queremos saber quem a iniciou.
import java.lang.ScopedValue;
public class ServerExample {
static final ScopedValue<String> USER = ScopedValue.newInstance();
public static void main(String[] args) {
processRequest("Alice");
processRequest("Bob");
}
static void processRequest(String userName) {
ScopedValue.where(USER, userName).run(() -> {
handleBusinessLogic();
});
}
static void handleBusinessLogic() {
System.out.println("Processando para o usuário: " + USER.get());
}
}
O que acontece:
- Para cada requisição é criado seu próprio escopo, no qual USER é “Alice” ou “Bob”.
- Dentro de handleBusinessLogic() sempre obtemos o nome de usuário correto.
- Assim que o processamento da requisição termina, o valor desaparece.
Exemplo 2: Log com contexto
Suponha que queiramos inserir automaticamente o identificador da requisição nos logs:
import java.lang.ScopedValue;
public class LoggingExample {
static final ScopedValue<String> REQUEST_ID = ScopedValue.newInstance();
public static void main(String[] args) {
for (int i = 1; i <= 3; i++) {
String reqId = "REQ-" + i;
ScopedValue.where(REQUEST_ID, reqId).run(() -> {
log("Início do processamento");
doWork();
log("Fim do processamento");
});
}
}
static void log(String message) {
System.out.printf("[%s] %s%n", REQUEST_ID.get(), message);
}
static void doWork() {
log("Trabalhando...");
}
}
Resultado (exemplo):
[REQ-1] Início do processamento
[REQ-1] Trabalhando...
[REQ-1] Fim do processamento
[REQ-2] Início do processamento
[REQ-2] Trabalhando...
[REQ-2] Fim do processamento
[REQ-3] Início do processamento
[REQ-3] Trabalhando...
[REQ-3] Fim do processamento
Cada escopo mantém seu próprio identificador de requisição, e nenhuma confusão entre threads é possível.
4. Scoped Values e threads virtuais: um par perfeito
Por que Scoped Values são especialmente úteis com threads virtuais
As threads virtuais vivem pouco — são criadas e destruídas aos milhares, às vezes em frações de segundo. Por isso, a abordagem antiga com ThreadLocal, em que os dados ficam “presos” à própria thread, simplesmente não funciona aqui: as threads desaparecem rápido demais, e o contexto pode vazar ou se confundir.
ScopedValue, ao contrário, vincula os dados à própria tarefa — ao seu escopo de execução. Isso significa que o contexto (por exemplo, o nome do usuário ou o ID da requisição) acompanha o código, não a thread. Quando a tarefa termina, o valor desaparece automaticamente. Para threads virtuais, é a solução ideal: segura, limpa e sem surpresas.
Exemplo: Processamento em massa de tarefas com threads virtuais
import java.lang.ScopedValue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class VirtualThreadScopedValueDemo {
static final ScopedValue<Integer> TASK_ID = ScopedValue.newInstance();
public static void main(String[] args) {
ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor();
for (int i = 1; i <= 10_000; i++) {
int taskId = i;
executor.submit(() -> ScopedValue.where(TASK_ID, taskId).run(() -> {
processTask();
}));
}
executor.shutdown();
}
static void processTask() {
// Cada tarefa tem seu próprio TASK_ID
System.out.println("Processando tarefa #" + TASK_ID.get());
}
}
Pontos-chave:
- Para cada tarefa é criado seu próprio escopo do valor TASK_ID.
- Mesmo que as tarefas sejam executadas em paralelo, os valores não se confundem entre threads.
- Sem vazamentos de memória: o escopo “morre” junto com a tarefa.
5. Comparação: ThreadLocal vs ScopedValue
| Critério | ThreadLocal | ScopedValue |
|---|---|---|
| Vinculação | À thread | Ao escopo do código (scope) |
| Ciclo de vida | Enquanto a thread estiver viva | Enquanto o scope estiver em execução |
| Segurança | Risco de vazamentos e confusão | Sem vazamentos, sem confusão |
| Threads virtuais | Ineficiente, arriscado | Ideal |
| Uso | |
|
| Aninhamento | Não suporta override | Permite sobrepor valores |
6. Escopos aninhados: sobreposição de valores
ScopedValue<String> INFO = ScopedValue.newInstance();
ScopedValue.where(INFO, "Externo").run(() -> {
System.out.println(INFO.get()); // "Externo"
ScopedValue.where(INFO, "Interno").run(() -> {
System.out.println(INFO.get()); // "Interno"
});
System.out.println(INFO.get()); // "Externo"
});
Resultado:
Externo
Interno
Externo
Isso é útil se, por exemplo, dentro de uma tarefa for preciso redefinir temporariamente o valor do contexto.
Scoped Values: cenários típicos de uso
- Passagem de identificador de usuário ou de requisição: para registrar ações em log ou verificar permissões.
- Log: inserção automática de contexto nos logs.
- Rastreamento: para debug e profiling.
- Parâmetros de transação: por exemplo, nível de isolamento ou modo de operação.
- Qualquer “contexto” visível apenas dentro de uma tarefa (ou de suas subtarefas).
7. Outros novos mecanismos: Structured Concurrency
Structured Concurrency é uma abordagem na qual tarefas relacionadas (por exemplo, subprocessos de uma mesma operação) são gerenciadas como um todo: se a tarefa pai finalizar ou falhar, todas as tarefas filhas são canceladas automaticamente. Isso reduz o risco de threads “esquecidas” ou “penduradas”.
Exemplo (bem esquemático):
try (var scope = StructuredTaskScope.ShutdownOnFailure()) {
Future<String> result1 = scope.fork(() -> fetchData1());
Future<String> result2 = scope.fork(() -> fetchData2());
scope.join(); // esperamos ambas concluírem
scope.throwIfFailed(); // se qualquer uma falhar — lançamos uma exceção
String combined = result1.resultNow() + result2.resultNow();
System.out.println(combined);
}
Vantagens:
- Gerenciamento mais limpo do ciclo de vida das tarefas.
- Sem subprocessos “pendurados”.
- Tratamento de erros mais fácil.
Structured Concurrency ainda está em modo de preview, mas já evolui ativamente.
8. Dicas práticas e limitações
Quando usar Scoped Values?
- Sempre que for preciso propagar contexto entre tarefas, especialmente com threads virtuais.
- Se antes você usava ThreadLocal — considere migrar para ScopedValue.
Quando o ThreadLocal ainda é necessário?
- Em casos raros, quando a thread vive por muito tempo e o contexto deve ser “permanente” por toda a sua vida útil (por exemplo, ao trabalhar com código legado).
Limitações
- Scoped Values não podem ser alterados após a criação do escopo — eles são “somente leitura”.
- Scoped Values não podem ser usados fora do escopo: tentar obter o valor fora da área lançará uma exceção.
- Não use Scoped Values para armazenar objetos grandes — o escopo deve ser leve e rápido.
9. Erros comuns ao usar Scoped Values
Erro nº 1: tentar obter o valor fora do escopo. Se você chamar USER.get() fora do bloco ScopedValue.where(...), receberá a exceção NoSuchElementException. Verifique se o acesso ocorre apenas dentro do escopo.
Erro nº 2: tentar alterar o valor dentro do escopo. Scoped Values não é um contêiner mutável. Se precisar “redefinir” temporariamente o valor, crie um escopo aninhado.
Erro nº 3: usar ThreadLocal e ScopedValue juntos. Não misture essas mecânicas sem necessidade extrema — isso pode levar a confusão e erros de contexto.
Erro nº 4: esquecer de envolver a lógica no bloco run(). Se você escreveu ScopedValue.where(USER, "Alice") sem .run(() -> { ... }), nenhum escopo será criado!
Erro nº 5: tentar usar Scoped Value para informações globais de longa duração. Para esses fins, é melhor usar variáveis comuns ou ThreadLocal (se houver justificativa).
GO TO FULL VERSION