Encodera

Спасибо, что читаете!

Пожертвуйте сайту, мы стараемся ради вас.

Оглавление

Ограниченные значения (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().

Спасибо, что читаете!

Пожертвуйте сайту, мы стараемся ради вас.