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

JEP 481: Scoped Values (Third Preview)

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

ОтветственныйAndrew Haley
ТипFeature
ОбластьSE
СтатусClosed / Delivered
Выпуск23
Компонентcore-libs
Обсуждениеloom dash dev at openjdk dot org
Связан сJEP 464: Scoped Values (Second Preview)
JEP 487: Scoped Values (Fourth Preview)
РецензентыAlan Bateman
ОдобренPaul Sandoz
Создан2024/04/24 14:31
Обновлён2025/02/25 16:32
Задача8331056

Аннотация

Мы вводим Scoped Values. С их помощью метод может передавать неизменяемые данные как вызываемым им методам в пределах потока, так и дочерним потокам. Поведение Scoped Values проще понять, чем поведение локальных переменных потока. Кроме того, они требуют меньше памяти и времени, особенно вместе с Virtual Threads (виртуальные потоки) (JEP 444) и Structured Concurrency (структурированная конкурентность) (JEP 480). Это API в статусе Preview.

История

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

Здесь мы предлагаем ещё раз выпустить этот API в статусе Preview в JDK 23, чтобы накопить больше опыта и получить больше отзывов. Изменение одно:

  • Тип параметра, задающего операцию, у метода ScopedValue.callWhere теперь новый функциональный интерфейс, по которому компилятор Java может определить, может ли быть выброшено проверяемое исключение. После этого изменения метод ScopedValue.getWhere больше не нужен и удалён.

Цели

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

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

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

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

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

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

  • Требовать отказа от локальных переменных потока или объявлять устаревшим существующий 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() {
    return (UserInfo)framework.readKey("userInfo", context);// call framework
}

Фреймворк может хранить объект 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);
}

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

Локальные переменные потока для передачи данных

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

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

public class Framework {

    private final Application application;

    public Framework(Application app) { this.application = app; }
    
    private final static 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);
    }

}

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

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

Проблемы локальных переменных потока

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

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

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

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

К облегчённой передаче данных

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

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

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

Описание

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

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

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

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

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

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

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

Значение слова «scoped»

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

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

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

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

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

Пример веб-фреймворка со scoped-значениями

Показанный ранее код фреймворка легко переписать так, чтобы вместо локальной переменной потока использовалось scoped-значение. В точке (1) фреймворк объявляет scoped-значение вместо локальной переменной потока. В точке (2) метод serve вызывает ScopedValue.runWhere вместо метода set локальной переменной потока.

class Framework {

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

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

}

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

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

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

Повторная привязка scoped-значений

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

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

void foo() {
    ScopedValue.runWhere(X, "hello", () -> bar());
}

void bar() {
    System.out.println(X.get()); // prints hello
    ScopedValue.runWhere(X, "goodbye", () -> 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-значений

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

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

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

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

@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-значения 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.runWhere. Principal остаётся в области видимости, пока выполняется дочерний поток, а scope.join гарантирует, что дочерние потоки завершатся до того, как runWhere сможет вернуть управление, уничтожив привязку. Это позволяет избежать проблемы неограниченного времени жизни, которая возникает при использовании локальных переменных потока. Устаревшие классы управления потоками, такие как ForkJoinPool, не поддерживают наследование scoped-значений, потому что не могут гарантировать, что дочерний поток, порождённый в некоторой области видимости родительского потока, завершится до того, как родитель покинет эту область.

Переход на scoped-значения

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

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

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

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

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

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

API ScopedValue

Полный API ScopedValue богаче небольшого подмножества, описанного выше. Хотя здесь мы приводим только примеры с ScopedValue<V>.runWhere(V, <value>, aRunnable), есть и другие способы привязать scoped-значение. Например, API также предоставляет вариант, который возвращает значение и может также выбросить Exception:

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

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

Следующий пример выполняет операцию, в которой k1 привязан (или перепривязан) к v1, а k2 привязан (или перепривязан) к v2:

ScopedValue.where(k1, v1).where(k2, v2).run(
        () -> ... );

Это и эффективнее, и намного легче читается, чем вложенные вызовы ScopedValue.runWhere.

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

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

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

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

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