JEP 487: Scoped Values (Fourth Preview)
Scoped Values (значения с ограниченной областью видимости), четвёртая версия Preview (предварительная версия)
| Ответственный | Andrew Haley |
| Тип | Feature |
| Область | SE |
| Статус | Closed / Delivered |
| Выпуск | 24 |
| Компонент | core-libs |
| Обсуждение | loom dash dev at openjdk dot org |
| Связан с | JEP 481: Scoped Values (Third Preview) |
| JEP 506: Scoped Values | |
| Рецензенты | Alan Bateman, Brian Goetz |
| Одобрен | Alan Bateman |
| Создан | 2024/08/15 16:18 |
| Обновлён | 2025/04/10 18:03 |
| Задача | 8338456 |
Аннотация
Вводятся Scoped Values, которые позволяют методу разделять неизменяемые данные как с вызываемыми им методами внутри потока, так и с дочерними потоками. Scoped Values проще для понимания, чем поточно-локальные переменные. Кроме того, они требуют меньше памяти и времени, особенно при использовании вместе с Virtual Threads (виртуальные потоки) из JEP 444 и Structured Concurrency (структурированная конкурентность) из JEP 480. Это API в статусе Preview.
История
API Scoped Values был предложен в статусе Incubator (инкубационный модуль) в JEP 429 (JDK 20), предложен в статусе Preview в JEP 446 (JDK 21), а затем улучшен и доработан в JEP 464 (JDK 22) и JEP 481 (JDK 23).
Здесь мы предлагаем ещё раз выпустить API в статусе Preview в JDK 24, чтобы получить дополнительный опыт и отзывы, с одним дополнительным изменением:
- Мы удалили методы
callWhereиrunWhereиз классаScopedValue, и API стал полностью текучим (fluent). Использовать одно или несколько привязанных значений Scoped Values теперь можно только через методыScopedValue.Carrier.callиScopedValue.Carrier.run.
Цели
-
Простота использования — должно быть легко понимать, как движутся данные.
-
Понятность — время жизни разделяемых данных должно быть очевидно из синтаксической структуры кода.
-
Надёжность — данные, разделяемые вызывающим методом, должны быть доступны только легитимным вызываемым методам.
-
Производительность — данные должны эффективно разделяться между большим числом потоков.
Что не является целью
-
Изменение языка программирования 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() {
// 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);
}
Пользовательский код никак не может помочь правильно обращаться с объектом контекста. В худшем случае он может помешать, перепутав контексты; в лучшем — он вынужден добавлять ещё один параметр во все методы, которые в итоге могут обратиться обратно к фреймворку. Если необходимость передавать контекст возникает при переработке фреймворка, то для её добавления сигнатуру придётся изменить не только непосредственным клиентам — пользовательским методам, которые напрямую вызывают методы фреймворка или напрямую вызываются им, — но и всем промежуточным методам, хотя контекст — это внутренняя деталь реализации фреймворка и пользовательский код не должен с ним взаимодействовать.
Поточно-локальные переменные для разделения данных
Традиционно разработчики использовали поточно-локальные переменные, появившиеся в Java 1.2, чтобы разделять данные между методами в стеке вызовов, не прибегая к параметрам методов. Поточно-локальная переменная — это переменная типа ThreadLocal. Хотя она выглядит как обычная переменная, у поточно-локальной переменной есть своё текущее значение в каждом потоке; какое именно значение используется, зависит от того, какой поток вызывает её методы get или set, чтобы прочитать или записать значение. Обычно поточно-локальная переменная объявляется как поле static final, а её доступность устанавливается в 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 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);
}
}
Благодаря поточно-локальной переменной не нужно передавать FrameworkContext как аргумент метода, когда фреймворк вызывает пользовательский код и когда пользовательский код обращается обратно к методу фреймворка. Поточно-локальная переменная служит скрытым параметром метода: поток, который вызывает CONTEXT.set в Framework.serve, а затем CONTEXT.get в Framework.readKey, автоматически увидит собственную локальную копию переменной CONTEXT. По сути, поле ThreadLocal служит ключом, по которому ищется значение FrameworkContext для текущего потока.
Хотя у ThreadLocals в каждом потоке установлено своё значение, значение, установленное в данный момент в одном потоке, может автоматически наследоваться другим потоком, который создаёт текущий поток, если использовать класс InheritableThreadLocal вместо класса ThreadLocal.
Проблемы поточно-локальных переменных
К сожалению, у поточно-локальных переменных есть три присущих им изъяна в дизайне.
-
Неограниченная изменяемость — каждая поточно-локальная переменная изменяема: любой код, который может вызвать метод
getпоточно-локальной переменной, может в любой момент вызвать методsetэтой переменной. Это верно даже тогда, когда объект в поточно-локальной переменной неизменяем, потому что все его поля объявлены как final. APIThreadLocalдопускает это, чтобы поддерживать полностью универсальную модель взаимодействия, в которой данные могут передаваться между методами в любом направлении. Это может приводить к запутанным, как спагетти, потокам данных и к программам, в которых трудно понять, какой метод обновляет разделяемое состояние и в каком порядке. Чаще же нужна, как в примере выше, простая односторонняя передача данных от одного метода другим. -
Неограниченное время жизни — после того как копия поточно-локальной переменной для потока установлена методом
set, установленное значение сохраняется на всё время жизни потока или пока код в потоке не вызовет методremove. К сожалению, разработчики часто забывают вызватьremove, поэтому данные потока нередко хранятся дольше, чем нужно. В частности, если используется пул потоков, значение поточно-локальной переменной, установленное в одной задаче, может, если его не очистить должным образом, случайно попасть в не связанную с ней задачу, что потенциально может привести к опасным уязвимостям. Кроме того, в программах, которые полагаются на неограниченную изменяемость поточно-локальных переменных, может не найтись чёткого момента, когда потоку безопасно вызватьremove; это может вызвать долговременную утечку памяти, поскольку данные потока не будут собраны сборщиком мусора, пока поток не завершится. Было бы лучше, если бы запись и чтение данных потока происходили в ограниченный период во время выполнения потока, что исключило бы возможность утечек. -
Дорогое наследование — накладные расходы поточно-локальных переменных могут быть ещё выше при большом числе потоков, поскольку поточно-локальные переменные родительского потока могут наследоваться дочерними потоками. (На самом деле поточно-локальная переменная не является локальной для одного потока.) Когда разработчик решает создать дочерний поток, наследующий поточно-локальные переменные, дочерний поток должен выделить память под каждую поточно-локальную переменную, ранее записанную в родительском потоке. Это может заметно увеличить потребление памяти. Дочерние потоки не могут разделять память, используемую родительским потоком, потому что API
ThreadLocalтребует, чтобы изменение копии поточно-локальной переменной в одном потоке не было видно в других потоках. Это досадно, так как на практике дочерние потоки редко вызывают методsetдля унаследованных поточно-локальных переменных.
К легковесному разделению данных
Проблемы поточно-локальных переменных стали острее с появлением Virtual Threads (JEP 444). Virtual Threads — это легковесные потоки, реализованные в JDK. Множество потоков Virtual Threads разделяют один и тот же поток операционной системы, что позволяет иметь очень большое число потоков Virtual Threads. Помимо того что потоков Virtual Threads может быть очень много, они достаточно дёшевы, чтобы представлять любую конкурентную единицу поведения. Это значит, что веб-фреймворк может выделить новый поток Virtual Threads под задачу обработки запроса и при этом обрабатывать тысячи или миллионы запросов одновременно. В нашем сквозном примере методы Framework.serve, Application.handle и Framework.readKey выполнялись бы в новом потоке Virtual Threads для каждого входящего запроса.
Было бы полезно, чтобы эти методы могли разделять данные независимо от того, выполняются ли они в потоках Virtual Threads или в традиционных платформенных потоках. Поскольку потоки Virtual Threads являются экземплярами Thread, у потока Virtual Threads могут быть поточно-локальные переменные; более того, из-за короткого времени жизни потоков Virtual Threads и того, что они не объединяются в пулы, упомянутая выше проблема долговременных утечек памяти становится менее острой. (Вызывать метод remove поточно-локальной переменной не нужно, когда поток быстро завершается, поскольку завершение автоматически удаляет его поточно-локальные переменные.) Однако если у каждого из миллиона потоков Virtual Threads есть собственная копия поточно-локальных переменных, потребление памяти может быть значительным.
Итак, поточно-локальные переменные сложнее, чем обычно требуется для разделения данных, и влекут значительные издержки, которых невозможно избежать. Платформа Java должна предоставлять способ хранить наследуемые данные потока для тысяч или миллионов потоков Virtual Threads. Если бы эти переменные потока были неизменяемыми, их данные могли бы эффективно разделяться дочерними потоками. Кроме того, время жизни этих переменных потока должно быть ограничено: любые данные, разделённые через переменную потока, должны становиться недоступными, как только метод, изначально разделивший эти данные, завершится.
Описание
Scoped-значение — это объект-контейнер, который позволяет методу безопасно и эффективно разделять значение данных с методами, вызываемыми им напрямую и косвенно в том же потоке, а также с дочерними потоками, не прибегая к параметрам методов. Это переменная типа ScopedValue. Обычно она объявляется как поле static final, а её доступность задаётся как private, чтобы код в других классах не мог обращаться к ней напрямую.
Как и с поточно-локальной переменной, со scoped-значением связано несколько значений, по одному на поток. Какое именно значение используется, зависит от того, какой поток вызывает его методы. В отличие от поточно-локальной переменной, scoped-значение записывается один раз и доступно только в течение ограниченного периода выполнения потока.
Scoped-значение используется так, как показано ниже. Некоторый код вызывает ScopedValue.where, передавая scoped-значение и объект, к которому его нужно привязать. Цепочечный вызов метода run привязывает scoped-значение, предоставляя копию, специфичную для текущего потока, а затем выполняет лямбда-выражение, переданное в качестве аргумента. В течение времени жизни вызова run лямбда-выражение или любой метод, вызванный из этого выражения напрямую или косвенно, может прочитать scoped-значение с помощью его метода 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-значения. Такое ограниченное время жизни сильно упрощает рассуждения о поведении потоков. Односторонняя передача данных от вызывающего метода к вызываемым — как напрямую, так и косвенно — видна с первого взгляда. Нет метода set, который позволял бы удалённому коду изменить scoped-значение в любой момент. Это также помогает производительности: чтение scoped-значения с помощью get часто выполняется так же быстро, как чтение локальной переменной, независимо от расстояния в стеке между вызывающим и вызываемым методами.
В остальных примерах мы предполагаем, что ScopedValue.where импортирован статически, вот так:
import static java.lang.ScopedValue.where;
Это позволяет нам сократить ScopedValue.where(NAME, <value>).run(...) до where(NAME, <value>).run(...)
Смысл слова «scoped»
Область видимости сущности — это пространство, в котором она существует, — пределы или диапазон, в которых её можно использовать. Например, в языке программирования Java область видимости объявления переменной — это часть текста программы, в которой допустимо обращаться к переменной по простому имени (JLS §6.3). Такую область видимости точнее называть лексической областью видимости или статической областью видимости, поскольку часть программы, в которой переменная находится в области видимости, можно определить статически, найдя символы { и } в тексте программы.
Другой вид области видимости называется динамической областью видимости. Динамическая область видимости сущности — это те части программы, которые могут использовать эту сущность во время выполнения программы. Если метод a вызывает метод b, который, в свою очередь, вызывает метод c, то время выполнения c содержится внутри выполнения b, которое содержится внутри выполнения a, хотя эти три метода — отдельные единицы кода:
|
| +–– a
| |
| | +–– b
| | |
TIME | | +–– c
| | | |
| | | |__
| | |
| | |__
| |
| |__
|
v
Именно к этому понятию отсылает название scoped value, поскольку привязка scoped-значения V в методе run (или call) даёт значение, доступное определённым частям программы во время её выполнения, а именно методам, вызванным напрямую или косвенно из run или call.
Разворачивающееся выполнение этих методов определяет динамическую область видимости; привязка действует во время выполнения этих методов и больше нигде.
Пример веб-фреймворка со scoped-значениями
Код фреймворка, показанный ранее, легко переписать так, чтобы он использовал scoped-значение вместо поточно-локальной переменной:
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-значение вместо поточно-локальной переменной. В точке (2) метод serve вызывает where ... run вместо метода set поточно-локальной переменной.
Метод run обеспечивает одностороннее разделение данных от метода serve к методу readKey. Scoped-значение, переданное в run, привязано к соответствующему объекту на время жизни вызова run, поэтому CONTEXT.get() в любом методе, вызванном из run, прочитает это значение. Соответственно, когда Framework.serve вызывает пользовательский код, а пользовательский код вызывает Framework.readKey, значение, прочитанное из scoped-значения (3), — это значение, записанное Framework.serve ранее в этом потоке.
Привязка, установленная run, может использоваться только в коде, вызванном из run. Если бы CONTEXT.get() появился в Framework.serve после вызова run, было бы выброшено исключение, поскольку CONTEXT больше не привязан в этом потоке.
Как и прежде, фреймворк полагается на контроль доступа Java, чтобы ограничить доступ к своим внутренним данным: поле CONTEXT имеет приватный доступ, что позволяет фреймворку разделять информацию внутри себя между двумя своими методами. Эта информация недоступна пользовательскому коду и скрыта от него. Мы говорим, что объект ScopedValue — это capability-объект, который даёт коду, имеющему права доступа к нему, возможность привязывать или читать значение. Часто ScopedValue будет иметь доступ private, но иногда у него может быть доступ protected или доступ на уровне пакета, чтобы несколько взаимодействующих классов могли читать и привязывать значение.
Повторная привязка scoped-значений
То, что у scoped-значений нет метода set, означает, что вызывающий метод может использовать scoped-значение, чтобы надёжно передать значение вызываемым им методам в том же потоке. Однако бывают случаи, когда одному из вызываемых методов нужно использовать то же scoped-значение, чтобы передать другое значение своим собственным вызываемым методам. 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-значений
В примере веб-фреймворка под обработку каждого запроса выделяется отдельный поток, поэтому один и тот же поток выполняет часть кода фреймворка, затем пользовательский код разработчика приложения, затем ещё код фреймворка для доступа к базе данных. Однако пользовательский код может воспользоваться легковесностью потоков 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.run. Principal остаётся в области видимости, пока выполняется дочерний поток, а scope.join гарантирует, что дочерние потоки завершатся до того, как run сможет вернуть управление, уничтожив привязку. Так удаётся избежать проблемы неограниченного времени жизни, которая возникает при использовании поточно-локальных переменных. Старые классы управления потоками, такие как ForkJoinPool, не поддерживают наследование scoped-значений, поскольку не могут гарантировать, что дочерний поток, порождённый в некоторой области видимости родительского потока, завершится до того, как родительский поток покинет эту область видимости.
Переход на scoped-значения
Scoped-значения, вероятно, окажутся полезными и предпочтительными во многих сценариях, где сегодня используются поточно-локальные переменные. Помимо роли скрытых параметров методов, scoped-значения могут помочь в следующих случаях:
-
Реентерабельный код — иногда желательно обнаруживать рекурсию, например потому, что фреймворк не реентерабелен, или потому, что рекурсию нужно каким-то образом ограничить. Scoped-значение позволяет это сделать: установите его как обычно, с помощью
ScopedValue.run, а затем глубоко в стеке вызовов вызовите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>.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 созданы под влиянием того, как многие диалекты Lisp поддерживают свободные переменные с динамической областью видимости; в частности, того, как такие переменные ведут себя в многопоточной среде выполнения с глубоким связыванием, такой как Interlisp-D. Scoped Values улучшают свободные переменные Lisp, добавляя типобезопасность, неизменяемость, инкапсуляцию и эффективный доступ как внутри потока, так и между потоками.