JavaRush /Курсы /JAVA 25 SELF /Scoped Values и новые механики потоков (Java 21+)

Scoped Values и новые механики потоков (Java 21+)

JAVA 25 SELF
57 уровень , 4 лекция
Открыта

1. Почему ThreadLocal теряет актуальность

Для чего вообще нужен ThreadLocal?

В классической многопоточности, где потоки живут долго (например, на сервере), иногда нужно хранить собственные данные для каждого потока — такие, что не должны пересекаться с другими. Например, имя пользователя, ID запроса или временный буфер.

Чтобы это сделать, в Java появился ThreadLocal<T> — некое «личное пространство» потока, где можно хранить данные, не мешая соседям:

ThreadLocal<String> user = new ThreadLocal<>();

user.set("Alice"); // значение сохраняется только для этого потока
String name = user.get(); // вернёт "Alice" именно здесь, в других потоках — null

Почему ThreadLocal не дружит с виртуальными потоками

Виртуальные потоки живут совсем иначе, чем старые «тяжёлые» потоки. Они появляются и исчезают тысячами — иногда за доли миллисекунды. А ThreadLocal привязывает данные к конкретному потоку, будто тот будет жить вечно.

Когда виртуальный поток завершает работу, его данные в ThreadLocal могут остаться висеть в памяти — даже если сам поток уже давно умер. Это приводит к утечкам, ведь JVM не всегда знает, что эти значения больше никому не нужны.

А если потоки переиспользуются (например, в пулах), возможна ещё более неприятная ситуация: «чужой» контекст может случайно перейти к новому запросу. Представьте, пользователь Петя получает данные Васи — и привет, баги и уязвимости.

ThreadLocal отлично чувствует себя там, где потоков немного и они живут долго. Но с виртуальными потоками — это как пытаться хранить вещи в шкафу, который исчезает каждую секунду.

2. Scoped Values: новый способ передавать контекст

Scoped Values — это свежий инструмент из Java 21, который решает старую проблему ThreadLocal, но делает это элегантно. Вместо того чтобы хранить данные внутри потока, как в ThreadLocal, он «прикрепляет» их к области выполнения — то есть к конкретному участку кода. Значение живёт только пока выполняется этот участок, а затем автоматически исчезает, не оставляя следов в памяти.

import java.lang.ScopedValue;

ScopedValue<String> USER = ScopedValue.newInstance();

ScopedValue.where(USER, "Alice").run(() -> {
    System.out.println("Hello, " + USER.get()); // Выведет: Hello, Alice
});

Когда код выходит за пределы блока run, значение уже недоступно — попытка обратиться к нему вызовет исключение. Ничего очищать вручную не нужно.

Scoped Values не засоряют память, не путают контекст между потоками и позволяют создавать вложенные области, где внутренние значения временно перекрывают внешние. Это аккуратный, предсказуемый и безопасный способ передавать контекст, особенно в мире виртуальных потоков.

3. Примеры использования Scoped Values

Пример 1: Передача контекста пользователя

Допустим, у нас есть сервер, который обрабатывает запросы от разных пользователей. Для каждого запроса мы хотим знать, кто его инициировал.

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("Обрабатываем для пользователя: " + USER.get());
    }
}

Что произойдёт:

  • Для каждого запроса создаётся свой scope, в котором USER равен «Alice» или «Bob».
  • Внутри handleBusinessLogic() мы всегда получаем корректное имя пользователя.
  • Как только обработка запроса завершена, значение исчезает.

Пример 2: Логирование с контекстом

Допустим, мы хотим автоматически подставлять идентификатор запроса в логи:

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("Начало обработки");
                doWork();
                log("Конец обработки");
            });
        }
    }

    static void log(String message) {
        System.out.printf("[%s] %s%n", REQUEST_ID.get(), message);
    }

    static void doWork() {
        log("Работаем...");
    }
}

Результат (пример):

[REQ-1] Начало обработки
[REQ-1] Работаем...
[REQ-1] Конец обработки
[REQ-2] Начало обработки
[REQ-2] Работаем...
[REQ-2] Конец обработки
[REQ-3] Начало обработки
[REQ-3] Работаем...
[REQ-3] Конец обработки

Каждый scope хранит свой идентификатор запроса, и никакая путаница между потоками невозможна.

4. Scoped Values и виртуальные потоки: идеальная пара

Почему Scoped Values особенно полезны с виртуальными потоками

Виртуальные потоки живут недолго — они создаются и уничтожаются тысячами, иногда за доли секунды. Поэтому старый подход с ThreadLocal, где данные жёстко «привязаны» к самому потоку, здесь просто не работает: потоки слишком быстро исчезают, а контекст может случайно утечь или перепутаться.

ScopedValue, наоборот, привязывает данные к самой задаче — к её области выполнения. Это значит, что контекст (например, имя пользователя или ID запроса) следует за кодом, а не за потоком. Когда задача заканчивается, значение автоматически исчезает. Для виртуальных потоков это идеальное решение: безопасное, чистое и без сюрпризов.

Пример: Массовая обработка задач с виртуальными потоками

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() {
        // Для каждой задачи свой TASK_ID
        System.out.println("Обрабатываем задачу #" + TASK_ID.get());
    }
}

Ключевые моменты:

  • Для каждой задачи создаётся свой scope значения TASK_ID.
  • Даже если задачи выполняются параллельно, значения не путаются между потоками.
  • Нет утечек памяти: scope «умирает» вместе с задачей.

5. Сравнение: ThreadLocal vs ScopedValue

Критерий ThreadLocal ScopedValue
Привязка К потоку К области кода (scope)
Жизненный цикл Пока жив поток Пока выполняется scope
Безопасность Риск утечек, путаницы Нет утечек, нет путаницы
Виртуальные потоки Неэффективен, опасен Идеально подходит
Использование
set/get
where(...).run(...), get
Вложенность Не поддерживает override Можно перекрывать значения

6. Вложенные области (scopes): перекрытие значений

ScopedValue<String> INFO = ScopedValue.newInstance();

ScopedValue.where(INFO, "Внешний").run(() -> {
    System.out.println(INFO.get()); // "Внешний"
    ScopedValue.where(INFO, "Внутренний").run(() -> {
        System.out.println(INFO.get()); // "Внутренний"
    });
    System.out.println(INFO.get()); // "Внешний"
});

Результат:

Внешний
Внутренний
Внешний

Это удобно, если, например, внутри одной задачи нужно временно переопределить значение контекста.

Scoped Values: типичные сценарии использования

  • Передача идентификатора пользователя или запроса: чтобы логировать действия или проверять права.
  • Логирование: автоматическая подстановка контекста в логи.
  • Трассировка: для дебага и профилирования.
  • Параметры транзакций: например, уровень изоляции или режим работы.
  • Любой «контекст», видимый только в пределах одной задачи (или её подзадач).

7. Другие новые механики: Structured Concurrency

Structured Concurrency — это подход, при котором связанные задачи (например, подпроцессы одной операции) управляются как единое целое: если родительская задача завершилась или упала, все дочерние задачи автоматически отменяются. Это снижает риск «забытых» или «висящих» потоков.

Пример (очень схематично):

try (var scope = StructuredTaskScope.ShutdownOnFailure()) {
    Future<String> result1 = scope.fork(() -> fetchData1());
    Future<String> result2 = scope.fork(() -> fetchData2());

    scope.join(); // ждем завершения обеих
    scope.throwIfFailed(); // если хоть одна упала — бросаем исключение

    String combined = result1.resultNow() + result2.resultNow();
    System.out.println(combined);
}

Преимущества:

  • Более чистое управление жизненным циклом задач.
  • Нет «висящих» подпроцессов.
  • Легче обрабатывать ошибки.

Structured Concurrency пока находится в preview-режиме, но уже активно развивается.

8. Практические советы и ограничения

Когда использовать Scoped Values?

  • Всегда, когда нужно передавать контекст между задачами, особенно с виртуальными потоками.
  • Если раньше вы использовали ThreadLocal — задумайтесь, не лучше ли перейти на ScopedValue.

Когда всё ещё нужен ThreadLocal?

  • В редких случаях, когда поток живёт очень долго и контекст должен быть «постоянным» для всего его времени жизни (например, при работе с legacy-кодом).

Ограничения

  • Scoped Values нельзя изменять после создания scope — они «только для чтения».
  • Scoped Values нельзя использовать вне scope: попытка получить значение вне области вызовет исключение.
  • Не используйте Scoped Values для хранения больших объектов — область должна быть лёгкой и быстрой.

9. Типичные ошибки при использовании Scoped Values

Ошибка №1: попытка получить значение вне scope. Если вызвать USER.get() вне блока ScopedValue.where(...), вы получите исключение NoSuchElementException. Проверьте, что обращение происходит только внутри области.

Ошибка №2: попытка изменить значение внутри scope. Scoped Values — это не изменяемый контейнер. Если нужно временно «переопределить» значение, создайте вложенный scope.

Ошибка №3: использование ThreadLocal и ScopedValue вместе. Не стоит смешивать эти механики без крайней необходимости — это может привести к путанице и ошибкам в контексте.

Ошибка №4: забыли вложить логику в блок run(). Если вы написали ScopedValue.where(USER, "Alice") без .run(() -> { ... }), никакой scope не создастся!

Ошибка №5: попытка использовать Scoped Value для долгоживущей глобальной информации. Для таких целей лучше использовать обычные переменные или ThreadLocal (если оправдано).

1
Задача
JAVA 25 SELF, 57 уровень, 4 лекция
Недоступна
Конфиденциальность данных в корпоративной сети 🔒
Конфиденциальность данных в корпоративной сети 🔒
1
Задача
JAVA 25 SELF, 57 уровень, 4 лекция
Недоступна
Отслеживание заказов в службе доставки дронами 📦
Отслеживание заказов в службе доставки дронами 📦
1
Опрос
Virtual Threads, 57 уровень, 4 лекция
Недоступен
Virtual Threads
Virtual Threads
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ