Ограниченные значения (ScopedValue)
Бывают данные, которые нужны многим методам сразу: текущий пользователь, номер запроса, язык интерфейса. Передавать их параметром через пять уровней вызовов утомительно, а глобальная переменная не подходит — в многопоточной программе у каждого потока свой пользователь. Для этого в Java 25 есть ограниченные значения (scoped values): значение привязывают на время выполнения кода, и любой метод внутри него может это значение прочитать.
Первое значение
Ограниченное значение создают один раз, обычно в поле static final, методом ScopedValue.newInstance(). Затем ScopedValue.where(ключ, значение) привязывает значение, а run(...) выполняет код, внутри которого оно доступно через get():
static final ScopedValue<String> USER = ScopedValue.newInstance();
void main() {
ScopedValue.where(USER, "Анна").run(() -> showProfile());
}
void showProfile() {
IO.println("Профиль: " + USER.get());
}Результат:
Профиль: АннаМетод showProfile не получает пользователя параметром — он берёт его из USER. Само поле USER — не значение, а ключ, по которому значение находят.
Только внутри run
Привязка действует ровно столько, сколько выполняется run, — отсюда и название «ограниченное». После выхода из run значения снова нет. Проверить привязку можно методом isBound(), а orElse(...) вернёт запасное значение. Если нужен результат, вместо run вызывают call:
static final ScopedValue<String> USER = ScopedValue.newInstance();
void main() throws Exception {
String greeting = ScopedValue.where(USER, "Олег").call(() -> "Привет, " + USER.get());
IO.println(greeting);
IO.println("Привязано: " + USER.isBound());
IO.println("Пользователь: " + USER.orElse("гость"));
}Результат:
Привет, Олег
Привязано: false
Пользователь: гостьИзменить значение внутри run нельзя: метода set у ScopedValue нет. Данные текут только сверху вниз, и любой код видит то, что привязал вызывающий.
Вложенная привязка
Внутри run можно привязать к тому же ключу другое значение — например, администратор действует от имени пользователя. Новое значение видно только во вложенном run, а после него возвращается прежнее:
static final ScopedValue<String> USER = ScopedValue.newInstance();
void main() {
ScopedValue.where(USER, "admin").run(() -> {
log("начало работы");
ScopedValue.where(USER, "Анна").run(() -> log("смена пароля"));
log("конец работы");
});
}
void log(String action) {
IO.println("[" + USER.get() + "] " + action);
}Результат:
[admin] начало работы
[Анна] смена пароля
[admin] конец работыВместо ThreadLocal
Раньше такую задачу решали классом ThreadLocal — переменной, у которой в каждом потоке своё значение. У него есть недостатки: значение можно менять из любого места, его нужно не забыть удалить методом remove(), иначе оно «утечёт» к следующей задаче в пуле. А с миллионом виртуальных потоков миллион копий занимает много памяти. Ограниченное значение неизменяемо, исчезает само и дёшево. Каждая задача привязывает своё значение:
static final ScopedValue<String> REQUEST_ID = ScopedValue.newInstance();
String handle(String id) {
return ScopedValue.where(REQUEST_ID, id).call(() -> loadOrder());
}
String loadOrder() {
return "запрос " + REQUEST_ID.get() + ": заказ загружен";
}
void main() throws Exception {
try (ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor()) {
List<Future<String>> results = new ArrayList<>();
for (String id : List.of("A-1", "B-2", "C-3")) {
results.add(executor.submit(() -> handle(id)));
}
for (Future<String> result : results) {
IO.println(result.get());
}
}
}Результат:
запрос A-1: заказ загружен
запрос B-2: заказ загружен
запрос C-3: заказ загруженЗадачи выполняются одновременно, но каждая видит только свой номер запроса. Потоки, созданные через ExecutorService, привязку родителя не наследуют, поэтому where вызывают внутри задачи. Автоматически передаёт значения в дочерние задачи структурированная параллельность (StructuredTaskScope), но в Java 25 она ещё в режиме предварительного просмотра (preview).
Частая ошибка
Вызов get() вне run — например, если метод вызвали напрямую, а не изнутри привязки, — выбрасывает исключение:
static final ScopedValue<String> USER = ScopedValue.newInstance();
void main() {
showProfile();
}
void showProfile() {
IO.println("Профиль: " + USER.get());
}Результат:
Exception in thread "main" java.util.NoSuchElementException: ScopedValue not bound
at java.base/java.lang.ScopedValue.slowGet(ScopedValue.java:571)
at java.base/java.lang.ScopedValue.get(ScopedValue.java:564)
at Main.showProfile(Main.java:8)
at Main.main(Main.java:4)Если метод может работать и без привязки, используйте USER.orElse("гость") или проверку USER.isBound().