openjdk.ruOpenJDK на русском

JEP 506: Scoped Values

Scoped Values (значения с ограниченной областью видимости)

АвторAndrew Haley & Andrew Dinn
ОтветственныйAndrew Haley
ТипFeature
ОбластьSE
СтатусClosed / Delivered
Выпуск25
Компонентcore-libs
Обсуждениеloom dash dev at openjdk dot org
Связан сJEP 487: Scoped Values (Fourth Preview)
РецензентыAlan Bateman
ОдобренPaul Sandoz
Создан2025/03/24 10:10
Обновлён2025/06/06 13:57
Задача8352695

Аннотация

Мы вводим Scoped Values, с помощью которых метод может передавать неизменяемые данные вызываемым методам в том же потоке и дочерним потокам. Работу со Scoped Values проще анализировать, чем работу с thread-local-переменными. Кроме того, они обходятся дешевле по памяти и по времени, особенно в сочетании с Virtual Threads (виртуальные потоки) (JEP 444) и Structured Concurrency (структурированная конкурентность) (JEP 505).

История

API Scoped Values был предложен в статусе Incubator (инкубационный модуль) в JEP 429 (JDK 20), в статусе Preview (предварительная версия) в JEP 446 (JDK 21), а затем улучшен и доработан в JEP 464 (JDK 22), JEP 481 (JDK 23) и JEP 487 (JDK 24).

Здесь мы предлагаем окончательно утвердить API Scoped Values в JDK 25 с одним небольшим изменением: метод ScopedValue.orElse больше не принимает null в качестве аргумента.

Цели

  • Простота использования — потоки данных должно быть легко анализировать.

  • Понятность — время жизни общих данных должно быть видно из синтаксической структуры кода.

  • Надёжность — данные, которые передаёт вызывающий метод, должны быть доступны только легитимным вызываемым методам.

  • Производительность — данные должно быть возможно эффективно разделять между большим числом потоков.

Что не является целью

  • Изменение языка программирования Java не является целью.

  • Целью не является требовать отказа от thread-local-переменных или объявлять устаревшим существующий API ThreadLocal.

Мотивация

Приложения и библиотеки на Java устроены как наборы классов, содержащих методы. Эти методы взаимодействуют через вызовы методов.

Большинство методов позволяют вызывающему коду передать данные методу в виде параметров. Когда метод A хочет, чтобы метод B выполнил для него какую-то работу, он вызывает B с нужными параметрами, а B может передать часть этих параметров в C, и так далее. B может понадобиться включить в список параметров не только то, что нужно самому B, но и то, что B должен передать в C. Например, если B будет подготавливать и выполнять обращение к базе данных, ему может понадобиться переданный Connection, даже если сам B не будет использовать Connection напрямую.

Чаще всего такой подход «передавай то, что нужно косвенно вызываемым методам» — самый эффективный и удобный способ разделять данные. Однако иногда передать при начальном вызове все данные, которые могут понадобиться каждому косвенно вызываемому методу, практически невозможно.

Пример

В больших программах на Java распространён шаблон, при котором управление передаётся от одного компонента («фреймворка») к другому («коду приложения») и затем обратно. Например, веб-фреймворк может принимать входящие HTTP-запросы и затем вызывать обработчик приложения, чтобы обработать запрос. Обработчик приложения, в свою очередь, может вызывать фреймворк, чтобы прочитать данные из базы данных или обратиться к какому-либо другому HTTP-сервису.

@Override
public void handle(Request request, Response response) {
    // user code, called by framework
    ...
    var userInfo = readUserInfo();
    ...
}

private UserInfo readUserInfo() {
    // call framework
    return (UserInfo)framework.readKey("userInfo", context);
}

Фреймворк может хранить объект FrameworkContext, содержащий идентификатор аутентифицированного пользователя, идентификатор транзакции и т. д., и связывать его с текущей транзакцией. Все операции фреймворка используют объект FrameworkContext, но пользовательский код его не использует (и не имеет к нему отношения).

По сути, фреймворк должен иметь возможность передавать свой внутренний контекст из своего метода serve (который вызывает пользовательский метод handle) в свой метод readKey:

4. Framework.readKey <--------+ use context
3. Application.readUserInfo   |
2. Application.handle         |
1. Framework.serve  ----------+ create context

Самый простой способ сделать это — передавать объект в качестве аргумента во все методы цепочки вызовов:

@Override
void handle(Request request, Response response, FrameworkContext context) {
    ...
    var userInfo = readUserInfo(context);
    ...
}

private UserInfo readUserInfo(FrameworkContext context) {
    return (UserInfo)framework.readKey("userInfo", context);
}

Пользовательский код никак не может помочь правильно обращаться с объектом контекста. В худшем случае он может помешать, перепутав контексты; в лучшем — он вынужден добавлять ещё один параметр во все методы, которые в итоге могут обратиться обратно к фреймворку. Если необходимость передавать контекст возникнет при переработке фреймворка, то для её добавления придётся изменить сигнатуры не только непосредственных клиентов — тех пользовательских методов, которые напрямую вызывают методы фреймворка или которые он вызывает напрямую, — но и всех промежуточных методов, хотя контекст — это внутренняя деталь реализации фреймворка и пользовательский код не должен с ним взаимодействовать.

Thread-local-переменные для разделения данных

Традиционно разработчики используют thread-local-переменные, появившиеся в Java 1.2, чтобы разделять данные между методами в стеке вызовов, не прибегая к параметрам методов. Thread-local-переменная — это переменная типа ThreadLocal. Хотя она выглядит как обычная переменная, у thread-local-переменной есть по одному текущему значению на каждый поток; какое именно значение используется, зависит от того, какой поток вызывает её методы get или set, чтобы прочитать или записать значение. Обычно thread-local-переменная объявляется как поле static final, а её доступность устанавливается как private, так что разделение данных можно ограничить экземплярами одного класса или группы классов из одной кодовой базы.

Вот пример того, как два метода фреймворка, выполняющиеся в одном и том же потоке обработки запроса, могут использовать thread-local-переменную, чтобы разделять FrameworkContext. Фреймворк объявляет thread-local-переменную CONTEXT (1). Когда Framework.serve выполняется в потоке обработки запроса, он записывает подходящий FrameworkContext в thread-local-переменную (2), а затем вызывает пользовательский код. Если и когда пользовательский код вызывает Framework.readKey, этот метод читает thread-local-переменную (3), чтобы получить FrameworkContext потока обработки запроса.

public class Framework {

    private final Application application;

    public Framework(Application app) { this.application = app; }
    
    private static final ThreadLocal<FrameworkContext> CONTEXT 
                       = new ThreadLocal<>();    // (1)

    void serve(Request request, Response response) {
        var context = createContext(request);
        CONTEXT.set(context);                    // (2)
        Application.handle(request, response);
    }

    public PersistedObject readKey(String key) {
        var context = CONTEXT.get();              // (3)
        var db = getDBConnection(context);
        db.readKey(key);
    }

}

Благодаря thread-local-переменной не нужно передавать FrameworkContext в качестве аргумента метода, когда фреймворк вызывает пользовательский код и когда пользовательский код обращается обратно к методу фреймворка. Thread-local-переменная служит скрытым параметром метода: поток, который вызывает CONTEXT.set в Framework.serve, а затем CONTEXT.get в Framework.readKey, автоматически увидит собственную локальную копию переменной CONTEXT. По сути, поле ThreadLocal служит ключом, по которому ищется значение FrameworkContext для текущего потока.

Хотя у ThreadLocals в каждом потоке задано своё значение, значение, заданное в данный момент в одном потоке, может автоматически наследоваться другим потоком, который текущий поток создаёт, если использовать класс InheritableThreadLocal вместо класса ThreadLocal.

Проблемы thread-local-переменных

К сожалению, у thread-local-переменных есть три неотъемлемых недостатка в проектировании.

  • Неограниченная изменяемость — любая thread-local-переменная изменяема: любой код, который может вызвать метод get thread-local-переменной, может в любой момент вызвать и метод set этой переменной. Это так, даже если объект в thread-local-переменной неизменяем, потому что все его поля объявлены final. API ThreadLocal допускает это, чтобы поддерживать полностью общую модель взаимодействия, в которой данные могут передаваться между методами в любом направлении. Это может приводить к запутанным, как спагетти, потокам данных и к программам, в которых трудно понять, какой метод обновляет общее состояние и в каком порядке. Чаще же требуется, как в примере выше, простая односторонняя передача данных от одного метода другим.

  • Неограниченное время жизни — после того как копия thread-local-переменной в потоке задана методом set, заданное значение сохраняется на всё время жизни потока или до тех пор, пока код в потоке не вызовет метод remove. К сожалению, разработчики часто забывают вызывать remove, поэтому данные потока часто хранятся дольше, чем нужно. В частности, если используется пул потоков, значение thread-local-переменной, заданное в одной задаче, может, если его не очистить должным образом, случайно попасть в другую, не связанную задачу, что может привести к опасным уязвимостям. Кроме того, в программах, которые полагаются на неограниченную изменяемость thread-local-переменных, может не найтись чёткого момента, когда потоку безопасно вызвать remove; это может вызвать долговременную утечку памяти, поскольку данные потока не будут освобождены сборщиком мусора, пока поток не завершится. Было бы лучше, если бы запись и чтение данных потока происходили в течение ограниченного периода выполнения потока, что исключало бы возможность утечек.

  • Дорогое наследование — издержки thread-local-переменных могут быть ещё больше при использовании большого числа потоков, поскольку thread-local-переменные родительского потока могут наследоваться дочерними потоками. (Thread-local-переменная на самом деле не локальна для одного потока.) Когда разработчик решает создать дочерний поток, наследующий thread-local-переменные, дочерний поток должен выделить память под каждую thread-local-переменную, ранее записанную в родительском потоке. Это может заметно увеличить потребление памяти. Дочерние потоки не могут разделять память, используемую родительским потоком, потому что API ThreadLocal требует, чтобы изменение копии thread-local-переменной в одном потоке не было видно в других потоках. Это досадно, поскольку на практике дочерние потоки редко вызывают метод set у унаследованных thread-local-переменных.

К облегчённому разделению данных

С появлением Virtual Threads (JEP 444) проблемы thread-local-переменных стали острее. Virtual Threads — это легковесные потоки, реализованные в JDK. Множество потоков Virtual Threads используют один и тот же поток операционной системы, поэтому их может быть очень много. Потоки Virtual Threads не только многочисленны, но и настолько дёшевы, что могут представлять любую конкурентную единицу работы. Это значит, что веб-фреймворк может выделять новый поток Virtual Threads под задачу обработки каждого запроса и при этом обрабатывать тысячи или миллионы запросов одновременно. В нашем примере методы Framework.serve, Application.handle и Framework.readKey выполнялись бы в новом потоке Virtual Threads для каждого входящего запроса.

Было бы полезно, чтобы эти методы могли разделять данные независимо от того, выполняются они в потоках Virtual Threads или в традиционных платформенных потоках. Поскольку потоки Virtual Threads являются экземплярами Thread, у них могут быть thread-local-переменные; более того, поскольку потоки Virtual Threads живут недолго и не объединяются в пул, упомянутая выше проблема долговременных утечек памяти для них менее остра. (Вызывать метод remove thread-local-переменной не нужно, если поток быстро завершается, поскольку при завершении его thread-local-переменные удаляются автоматически.) Однако если у каждого из миллиона потоков Virtual Threads будет собственная копия thread-local-переменных, потребление памяти может оказаться значительным.

Итак, thread-local-переменные сложнее, чем обычно нужно для разделения данных, и влекут значительные издержки, которых невозможно избежать. Платформа Java должна предоставлять способ хранить наследуемые данные потока для тысяч или миллионов потоков Virtual Threads. Если бы такие переменные потока были неизменяемыми, их данные могли бы эффективно разделяться дочерними потоками. Кроме того, время жизни таких переменных потока должно быть ограничено: любые данные, разделяемые через переменную потока, должны становиться недоступными после завершения метода, который первоначально их предоставил.

Описание

Scoped value — это объект-контейнер, с помощью которого метод может безопасно и эффективно передавать значение данных своим прямо и косвенно вызываемым методам в том же потоке и дочерним потокам, не прибегая к параметрам методов. Это переменная типа ScopedValue. Обычно она объявляется как поле static final, а её доступность устанавливается как private, чтобы код в других классах не мог обращаться к ней напрямую.

Как и у thread-local-переменной, у scoped value есть несколько связанных с ней значений, по одному на поток. Какое именно значение используется, зависит от того, какой поток вызывает её методы. В отличие от thread-local-переменной, scoped value записывается один раз и доступна только в течение ограниченного периода выполнения потока.

Scoped value используется, как показано ниже. Некоторый код вызывает ScopedValue.where, передавая scoped value и объект, к которому её нужно привязать. Следующий в цепочке вызов метода run привязывает scoped value, создавая копию, относящуюся к текущему потоку, а затем выполняет лямбда-выражение, переданное как аргумент. В течение времени жизни вызова run лямбда-выражение или любой метод, вызванный из этого выражения прямо или косвенно, может прочитать scoped value с помощью её метода get. После завершения метода run привязка уничтожается.

static final ScopedValue<...> NAME = ScopedValue.newInstance();

// In some method:
ScopedValue.where(NAME, <value>).run(() -> { ... NAME.get() ... call methods ... });

// In a method called directly or indirectly from the lambda expression:
... NAME.get() ...

Структура кода очерчивает период времени, в течение которого поток может читать свою копию scoped value. Такое ограниченное время жизни значительно упрощает рассуждения о поведении потоков. Односторонняя передача данных от вызывающего метода к вызываемым, как прямо, так и косвенно, видна с первого взгляда. Нет метода set, с помощью которого удалённый код мог бы в любой момент изменить scoped value. Это также помогает производительности: чтение scoped value с помощью get часто выполняется так же быстро, как чтение локальной переменной, независимо от расстояния в стеке между вызывающим и вызываемым методами.

В остальных примерах мы предполагаем, что ScopedValue.where импортирован статически, вот так:

import static java.lang.ScopedValue.where;

Так мы можем сократить ScopedValue.where(NAME, <value>).run(...) до where(NAME, <value>).run(...)

Что означает «scoped»

Область видимости (scope) чего-либо — это пространство, в котором оно существует, то есть пределы или диапазон, в которых его можно использовать. Например, в языке программирования Java область видимости объявления переменной — это часть текста программы, в которой допустимо ссылаться на переменную по простому имени (JLS §6.3). Такую область видимости точнее называть лексической или статической, поскольку часть программы, где переменная находится в области видимости, можно определить статически, найдя в тексте программы символы { и }.

Другой вид области видимости называется динамической областью видимости. Динамическая область видимости чего-либо — это те части программы, которые могут использовать его во время выполнения программы. Если метод a вызывает метод b, который, в свою очередь, вызывает метод c, то время выполнения c содержится внутри выполнения b, которое содержится внутри выполнения a, хотя эти три метода — отдельные единицы кода:

|
  |   +–– a
  |   |
  |   |  +–– b
  |   |  |
TIME  |  |  +–– c
  |   |  |  |
  |   |  |  |__
  |   |  |
  |   |  |__
  |   |
  |   |__
  |
  v

Именно к этому понятию отсылает название scoped value, поскольку привязка scoped value V в методе run (или call) создаёт значение, доступное определённым частям программы во время её выполнения, а именно методам, прямо или косвенно вызываемым из run или call.

Разворачивающееся выполнение этих методов определяет динамическую область видимости; привязка находится в области видимости во время выполнения этих методов и больше нигде.

Пример веб-фреймворка со Scoped Values

Код фреймворка, показанный ранее, можно легко переписать так, чтобы вместо thread-local-переменной использовалась scoped value:

class Framework {

    private static final ScopedValue<FrameworkContext> CONTEXT
                        = ScopedValue.newInstance();    // (1)

    void serve(Request request, Response response) {
        var context = createContext(request);
        where(CONTEXT, context)                         // (2)
                   .run(() -> Application.handle(request, response));
    }
    
    public PersistedObject readKey(String key) {
        var context = CONTEXT.get();                    // (3)
        var db = getDBConnection(context);
        db.readKey(key);
    }

}

В точке (1) фреймворк объявляет scoped value вместо thread-local-переменной. В точке (2) метод serve вызывает where ... run вместо метода set thread-local-переменной.

Метод run обеспечивает одностороннюю передачу данных из метода serve в метод readKey. Scoped value, переданная в run, привязана к соответствующему объекту на время жизни вызова run, поэтому CONTEXT.get() в любом методе, вызванном из run, прочитает это значение. Соответственно, когда Framework.serve вызывает пользовательский код, а пользовательский код вызывает Framework.readKey, значение, прочитанное из scoped value (3), — это значение, записанное ранее в этом потоке методом Framework.serve.

Привязка, установленная run, может использоваться только в коде, вызванном из run. Если бы CONTEXT.get() встречался в Framework.serve после вызова run, было бы выброшено исключение, поскольку CONTEXT больше не привязана в этом потоке.

Как и прежде, фреймворк полагается на контроль доступа Java, чтобы ограничить доступ к своим внутренним данным: поле CONTEXT имеет закрытый доступ, что позволяет фреймворку разделять информацию внутри себя между двумя своими методами. Эта информация недоступна пользовательскому коду и скрыта от него. Мы говорим, что объект ScopedValue является объектом-полномочием (capability): код, имеющий право доступа к нему, может привязывать или читать значение. Часто у ScopedValue будет доступ private, но иногда он может иметь доступ protected или пакетный доступ, чтобы несколько взаимодействующих классов могли читать и привязывать значение.

Повторная привязка Scoped Values

Отсутствие у Scoped Values метода set означает, что вызывающий метод может с помощью scoped value надёжно передавать значение вызываемым методам в том же потоке. Однако бывают случаи, когда одному из вызываемых методов нужно с помощью той же scoped value передать другое значение своим вызываемым методам. API ScopedValue позволяет установить новую вложенную привязку для последующих вызовов:

private static final ScopedValue<String> X = ScopedValue.newInstance();

void foo() {
   where(X, "hello").run(() -> bar());
}

void bar() {
    System.out.println(X.get()); // prints hello
    where(X, "goodbye").run(() -> baz());
    System.out.println(X.get()); // prints hello
}

void baz() {
    System.out.println(X.get()); // prints goodbye
}

bar читает значение X, равное "hello", поскольку такова привязка в области видимости, установленной в foo. Но затем bar устанавливает вложенную область видимости для выполнения baz, в которой X привязана к goodbye.

Обратите внимание, что привязка "goodbye" действует только внутри вложенной области видимости. Как только baz возвращает управление, значение X внутри bar снова становится '"hello"'. Тело bar не может изменить привязку, которую видит сам этот метод, но может изменить привязку, которую видят вызываемые им методы. После выхода из foo X снова становится непривязанной. Такая вложенность гарантирует ограниченное время жизни для разделения нового значения.

Наследование Scoped Values

В примере веб-фреймворка каждому запросу выделяется отдельный поток, поэтому один и тот же поток выполняет сначала код фреймворка, затем пользовательский код разработчика приложения, а затем снова код фреймворка для доступа к базе данных. Однако пользовательский код может воспользоваться легковесностью потоков Virtual Threads, создавая собственные потоки Virtual Threads и выполняя в них свой код. Эти потоки Virtual Threads будут дочерними потоками потока, обрабатывающего запрос.

Контекстные данные, разделяемые кодом, выполняющимся в потоке обработки запроса, должны быть доступны коду, выполняющемуся в дочерних потоках. Иначе пользовательский код, выполняющийся в дочернем потоке, при вызове метода фреймворка не сможет получить доступ к FrameworkContext, созданному кодом фреймворка в потоке обработки запроса. Чтобы данные можно было разделять между потоками, Scoped Values могут наследоваться дочерними потоками.

Предпочтительный механизм создания потоков Virtual Threads в пользовательском коде — Structured Concurrency API (JEP 505), а именно класс StructuredTaskScope. Scoped Values родительского потока автоматически наследуются дочерними потоками, созданными с помощью StructuredTaskScope. Код в дочернем потоке может использовать привязки, установленные для scoped value в родительском потоке, с минимальными накладными расходами. В отличие от thread-local-переменных, привязки scoped value родительского потока не копируются в дочерний поток.

Вот пример наследования scoped value, которое происходит в пользовательском коде незаметно. Метод Server.serve привязывает CONTEXT и вызывает Application.handle, как и раньше. Однако пользовательский код в Application.handle выполняет методы readUserInfo и fetchOffers конкурентно, каждый в своём потоке Virtual Threads, с помощью StructuredTaskScope.fork (1, 2). Каждый метод может использовать Framework.readKey, который, как и прежде, обращается к scoped value CONTEXT (4). Дальнейшие подробности пользовательского кода здесь не рассматриваются; подробнее см. JEP 505.

@Override
public Response handle(Request request, Response response) {
      try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
          Supplier<UserInfo>     user  = scope.fork(() -> readUserInfo());  // (1)
          Supplier<List<Offer>> offers = scope.fork(() -> fetchOffers());   // (2)
          scope.join().throwIfFailed();  // Wait for both forks
          return new Response(user.get(), order.get());
      } catch (Exception ex) {
          reportError(response, ex);
      }
}

StructuredTaskScope.fork гарантирует, что привязка scoped value CONTEXT, сделанная в потоке обработки запроса (в Framework.serve), будет прочитана CONTEXT.get в дочернем потоке. На следующей схеме показано, как динамическая область видимости привязки распространяется на все методы, выполняемые в дочернем потоке:

Thread 1                        Thread 2
--------                        --------   
                                5. Framework.readKey <----------+
                                                                |
                                                              CONTEXT
                                4. Application.readUserInfo     |
3. StructuredTaskScope.fork                                     |
2. Application.handle                                           |
1. Server.serve     --------------------------------------------+

Модель fork/join, которую предлагает StructuredTaskScope, означает, что динамическая область видимости привязки по-прежнему ограничена временем жизни вызова ScopedValue.run. Principal будет оставаться в области видимости, пока выполняется дочерний поток, а scope.join гарантирует, что дочерние потоки завершатся до того, как run сможет вернуть управление и уничтожить привязку. Так удаётся избежать проблемы неограниченного времени жизни, возникающей при использовании thread-local-переменных. Устаревшие классы управления потоками, такие как ForkJoinPool, не поддерживают наследование Scoped Values, поскольку не могут гарантировать, что дочерний поток, порождённый в некоторой области видимости родительского потока, завершится до того, как родительский поток покинет эту область.

Переход на Scoped Values

Scoped Values, вероятно, окажутся полезными и предпочтительными во многих сценариях, где сегодня используются thread-local-переменные. Помимо роли скрытых параметров методов, Scoped Values могут помочь в следующих случаях:

  • Реентерабельный код — иногда желательно обнаруживать рекурсию, например потому, что фреймворк не является реентерабельным или потому, что рекурсию нужно как-то ограничить. Scoped value даёт способ это сделать: настройте её как обычно, с помощью ScopedValue.run, а затем глубоко в стеке вызовов вызовите ScopedValue.isBound, чтобы проверить, есть ли у неё привязка для текущего потока. В более сложном варианте scoped value может моделировать счётчик рекурсии, если её многократно привязывать заново.

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

  • Графические контексты — ещё один пример встречается в графике, где часто есть контекст рисования, который нужно разделять между частями программы. Благодаря автоматической очистке и реентерабельности Scoped Values подходят для этого лучше, чем thread-local-переменные.

В целом мы советуем переходить на Scoped Values, когда назначение thread-local-переменной совпадает с назначением scoped value: односторонней передачей неизменяемых данных. Если в кодовой базе thread-local-переменные используются для двусторонней передачи (когда вызываемый метод глубоко в стеке вызовов передаёт данные удалённому вызывающему методу через ThreadLocal.set) или совершенно неструктурированно, то переход невозможен.

Есть несколько сценариев, в которых предпочтительны thread-local-переменные. Пример — кэширование объектов, которые дорого создавать и использовать. Например, объекты java.text.SimpleDateFormat дорого создавать, и, как известно, они к тому же изменяемые, поэтому их нельзя разделять между потоками без синхронизации. Поэтому практичным подходом часто было давать каждому потоку собственный объект SimpleDateFormat через thread-local-переменную, которая существует на протяжении всего времени жизни потока. (Впрочем, сегодня любой код, кэширующий объект SimpleDateFormat, мог бы перейти на более новый java.util.time.DateTimeFormatter, который можно хранить в поле static final и разделять между потоками.)

API ScopedValue

Полный API ScopedValue богаче, чем описанное выше подмножество. Выше мы показывали только примеры с ScopedValue<V>.where(V, <value>).run(...), но API также предоставляет метод call, который возвращает значение и может также выбрасывать исключение:

try {
        var result = where(X, "hello").call(() -> bar());
        ... use result ...
    catch (Exception e) {
        handleFailure(e);
    }
        ...

Кроме того, в одном месте вызова можно привязать несколько Scoped Values:

where(X, v).where(Y, w).run(() -> ... );

В этом примере операция выполняется, когда X привязан (или перепривязан) к v, а Y привязан (или перепривязан) к w. Такой код и эффективнее, и читается легче, чем вложенные вызовы ScopedValue ... where ... run.

Полный API Scoped Values описан здесь.

Альтернативы

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

Мы экспериментировали с изменённой версией ThreadLocal, поддерживающей некоторые свойства Scoped Values. Однако из-за дополнительного багажа локальных переменных потока получается либо чрезмерно громоздкая реализация, либо API, который для значительной части основной функциональности возвращает UnsupportedOperationException, либо и то и другое. Поэтому лучше не изменять ThreadLocal, а ввести Scoped Values как совершенно отдельное понятие.

Мы также экспериментировали с вариантом привязки Scoped Values, который поддерживает интерфейс AutoCloseable и поэтому мог бы использоваться в конструкции try-with-resources. В итоге мы отказались от этой идеи, потому что невозможно гарантировать корректную работу, если полагаться на то, что пользовательский код вызовет метод close в нужный момент. Кроме того, даже если этот метод вызывается так, как задумано, то есть конструкцией try-with-resources, операция может сразу же завершиться ошибкой StackOverflowError и оставить программу в несогласованном состоянии. Целостность, даже в случае StackOverflowError, важнее удобства try-with-resources. Функциональные интерфейсы, которые используют методы run и call этого API, позволяют нам гарантировать целостность и при этом остаются достаточно удобными в использовании.

Идея Scoped Values навеяна тем, как многие диалекты Lisp поддерживают свободные переменные с динамической областью видимости, в частности тем, как такие переменные ведут себя в многопоточной среде выполнения с глубоким связыванием, такой как Interlisp-D. Scoped Values улучшают свободные переменные Lisp: они добавляют типобезопасность, неизменяемость, инкапсуляцию и эффективный доступ как внутри потоков, так и между ними.