Scoped Values — неизменяемый контекст вместо ThreadLocal
Тема дорожной карты · Java
Scoped values — механизм передачи неизменяемого контекста «сверху вниз» по цепочке вызовов без явных параметров в каждой сигнатуре и без ThreadLocal. После нескольких раундов инкубации и preview API финализирован в Java 25 (JEP 506) — это стабильная функциональность, никакие флаги не нужны. Типовой сценарий: в начале обработки запроса положить в контекст данные аутентифицированного пользователя, trace-id или транзакцию, а читать их глубоко в стеке — в сервисах, репозиториях, логировании.
Как это работает
private static final ScopedValue<User> CURRENT_USER = ScopedValue.newInstance();
void serve(Request request) {
User user = authenticate(request);
ScopedValue.where(CURRENT_USER, user) // связывание
.run(() -> handle(request)); // видно всей цепочке вызовов
}
void audit(String action) {
log.info(CURRENT_USER.get().name() + ": " + action);
}
Связывание существует только на время выполнения run()/call() и снимается автоматически, даже если код завершился исключением. Значение неизменяемо: метода set() нет вовсе, «поменять» его можно только перевязав во вложенном where (rebinding), и после выхода из вложенного блока снова видна прежняя привязка. Чтение вне области связывания бросает NoSuchElementException; для необязательного контекста есть isBound() и orElse(). Главное отличие от InheritableThreadLocal: подзадачи, порождённые через fork() в структурированном параллелизме, видят scoped values родителя без копирования — наследуется ссылка на неизменяемые данные. Поэтому механизм остаётся практически бесплатным даже при миллионах виртуальных потоков, где копирующее наследование стало бы заметной статьёй расходов памяти.
У ThreadLocal три хронические проблемы: неограниченная мутабельность (записать значение может любой код в любой момент), утечки в пулах, где поток живёт дольше задачи и продолжает хранить чужие данные, и дорогое наследование с копированием таблицы значений в каждый дочерний поток. Scoped values закрывают все три разом — неизменяемостью и жёсткой привязкой значения к области выполнения.
Когда применять
- Контекст запроса: текущий пользователь, локаль, trace-id, дедлайн — всё, что неизменно на время одной операции.
- Неявные параметры фреймворка: security-контекст, активная транзакция, метаданные запроса.
- Везде, где раньше жил
ThreadLocalс ритуалом «set → try → finally remove»: у scoped values утечки такого рода невозможны по построению — привязка исчезает вместе с областью. - В сервисах на виртуальных потоках: дёшево создавать поток на запрос, не таская за каждым копию контекста.
Типичные ошибки
- Пытаться записать значение из глубины стека: API намеренно односторонний, данные текут только сверху вниз — это фича, а не ограничение.
- Читать без проверки
isBound()в коде, который вызывается и вне контекста, — получите исключение в рантайме. - Ждать наследования в задачах, отправленных в обычный
ExecutorService: привязки наследуются только подзадачами structured concurrency. - Хранить в
ScopedValueмутабельный объект: сам контейнер неизменяем, но за неизменяемость содержимого отвечаете вы. - Механически менять
ThreadLocalнаScopedValueтам, где значение реально мутирует по ходу обработки, — эти случаи требуют перепроектирования, а не замены типа.
Связанные понятия
Полезные ресурсы
Проверить знания (2)
Загрузка вопросов…