CodeGym /Cursos /JAVA 25 SELF /Scoped Values e novos mecanismos de threads (Java 21+)

Scoped Values e novos mecanismos de threads (Java 21+)

JAVA 25 SELF
Nível 57 , Lição 4
Disponível

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
set/get
where(...).run(...), get
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).

1
Pesquisa/teste
Virtual Threads, nível 57, lição 4
Indisponível
Virtual Threads
Virtual Threads
Comentários
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION