JEP 464: Scoped Values (Second Preview)
Scoped Values (значения с ограниченной областью видимости), вторая версия Preview (предварительная версия)
| Автор | Andrew Haley & Andrew Dinn |
| Ответственный | Andrew Haley |
| Тип | Feature |
| Область | SE |
| Статус | Closed / Delivered |
| Выпуск | 22 |
| Компонент | core-libs |
| Обсуждение | loom dash dev at openjdk dot org |
| Связан с | JEP 446: Scoped Values (Preview) |
| JEP 481: Scoped Values (Third Preview) | |
| Рецензенты | Alan Bateman |
| Одобрен | Paul Sandoz |
| Создан | 2023/10/26 12:05 |
| Обновлён | 2025/02/25 16:32 |
| Задача | 8318898 |
Аннотация
Вводятся Scoped Values, которые позволяют управляемо разделять неизменяемые данные как с дочерними фреймами в том же потоке, так и с дочерними потоками. Работу со Scoped Values проще понять, чем работу с локальными переменными потока, и они требуют меньше памяти и времени, особенно в сочетании с Virtual Threads (виртуальные потоки) и Structured Concurrency (структурированная конкурентность). Это API в статусе Preview.
История
Scoped Values были в статусе Incubator (инкубационный модуль) в JDK 20 (JEP 429) и стали API в статусе Preview в JDK 21 (JEP 446). Здесь мы предлагаем повторно выпустить этот API в статусе Preview в JDK 22 без изменений, чтобы накопить дополнительный опыт и получить больше отзывов.
Цели
-
Простота использования — поток данных должен быть лёгким для понимания.
-
Понятность — время жизни разделяемых данных видно из синтаксической структуры кода.
-
Надёжность — данные, разделяемые вызывающим методом, могут получить только легитимные вызываемые методы.
-
Производительность — данные можно эффективно разделять между большим числом потоков.
Что не является целью
-
Изменение языка программирования Java не является целью.
-
Целью не является требовать отказа от локальных переменных потока или помечать существующий API
ThreadLocalкак Deprecated (устаревший).
Мотивация
Приложения и библиотеки на 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 и с доступом 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. 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 и того, что они не объединяются в пул, описанная выше проблема долговременных утечек памяти становится менее острой. (Вызывать метод remove() локальной переменной потока не нужно, если поток быстро завершается, поскольку при завершении его локальные переменные потока удаляются автоматически.) Однако если у каждого из миллиона потоков Virtual Threads есть собственная копия локальных переменных потока, объём занимаемой памяти может быть значительным.
Если кратко, локальные переменные потока сложнее, чем обычно требуется для разделения данных, и влекут значительные издержки, которых нельзя избежать. Платформа Java должна предоставлять способ хранить наследуемые данные отдельно для каждого потока для тысяч или миллионов потоков Virtual Threads. Если бы такие переменные потока были неизменяемыми, их данные могли бы эффективно разделяться дочерними потоками. Кроме того, время жизни таких переменных потока должно быть ограничено: любые данные, разделяемые через такую переменную, должны становиться недоступными, как только метод, изначально разделивший эти данные, завершится.
Описание
Значение Scoped Values — это объект-контейнер, который позволяет методу безопасно и эффективно разделять значение данных со своими прямыми и косвенными вызываемыми методами в том же потоке, а также с дочерними потоками, не прибегая к параметрам методов. Это переменная типа ScopedValue. Обычно она объявляется как поле final static, а её доступность задаётся как private, чтобы код в других классах не мог обращаться к ней напрямую.
Как и у локальной переменной потока, у значения Scoped Values есть несколько связанных с ним значений, по одному на поток. Какое именно значение используется, зависит от того, какой поток вызывает его методы. В отличие от локальной переменной потока, значение Scoped Values записывается один раз и доступно только в течение ограниченного периода выполнения потока.
Значение Scoped Values используется так, как показано ниже. Некоторый код вызывает ScopedValue.where, передавая значение Scoped Values и объект, к которому его нужно привязать. Вызов run привязывает значение Scoped Values, создавая копию, специфичную для текущего потока, а затем выполняет лямбда-выражение, переданное в качестве аргумента. В течение времени жизни вызова run лямбда-выражение или любой метод, вызванный из этого выражения прямо или косвенно, может прочитать значение Scoped Values через его метод get(). После завершения метода run привязка уничтожается.
final static 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 Values. Такое ограниченное время жизни значительно упрощает рассуждения о поведении потока. Односторонняя передача данных от вызывающего метода к вызываемым — как прямым, так и косвенным — видна с первого взгляда. Нет метода set, который позволял бы удалённому коду в любой момент изменить значение Scoped Values. Это также помогает производительности: чтение значения Scoped Values с помощью get() часто так же быстро, как чтение локальной переменной, независимо от расстояния в стеке между вызывающим и вызываемым методами.
Значение слова «scoped»
Область видимости (scope) чего-либо — это пространство, в котором оно существует, — пределы или диапазон, в которых его можно использовать. Например, в языке программирования Java областью видимости объявления переменной является часть текста программы, в которой допустимо ссылаться на переменную по простому имени (JLS 6.3). Такую область видимости точнее называть лексической областью видимости или статической областью видимости, поскольку часть программы, где переменная находится в области видимости, можно определить статически, найдя символы { и } в тексте программы.
Другой вид области видимости называется динамической областью видимости. Динамическая область видимости чего-либо — это те части программы, которые могут использовать это в ходе выполнения программы. Если метод a вызывает метод b, который, в свою очередь, вызывает метод c, то время выполнения c содержится внутри выполнения b, которое содержится внутри выполнения a, хотя эти три метода — отдельные единицы кода:
|
| +–– a
| |
| | +–– b
| | |
TIME | | +–– c
| | | |
| | | |__
| | |
| | |__
| |
| |__
|
v
Именно к этому понятию отсылает термин scoped value, поскольку привязка значения Scoped Values V в методе run создаёт значение, доступное определённым частям программы в ходе её выполнения, а именно методам, вызываемым прямо или косвенно из run.
Разворачивающееся выполнение этих методов определяет динамическую область видимости; привязка действует в этой области во время выполнения этих методов и больше нигде.
Пример веб-фреймворка со значениями Scoped Values
Показанный ранее код фреймворка легко переписать так, чтобы вместо локальной переменной потока использовалось значение Scoped Values. В точке (1) фреймворк объявляет значение Scoped Values вместо локальной переменной потока. В точке (2) метод serve вызывает ScopedValue.where и run вместо метода set локальной переменной потока.
class Framework {
private final static ScopedValue<FrameworkContext> CONTEXT
= ScopedValue.newInstance(); // (1)
void serve(Request request, Response response) {
var context = createContext(request);
ScopedValue.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);
}
...
}
Вместе where и run обеспечивают одностороннее разделение данных от метода serve к методу readKey. Значение Scoped Values, переданное в where, привязано к соответствующему объекту на время жизни вызова run, поэтому CONTEXT.get() в любом методе, вызванном из run, прочитает это значение. Соответственно, когда Framework.serve вызывает пользовательский код, а пользовательский код вызывает Framework.readKey, значение, прочитанное из значения Scoped Values (3), — это значение, записанное ранее в этом потоке методом Framework.serve.
Привязка, установленная run, может использоваться только в коде, вызванном из run. Если бы CONTEXT.get() появился в Framework.serve после вызова run, было бы выброшено исключение, поскольку CONTEXT больше не привязан в этом потоке.
Как и раньше, фреймворк полагается на управление доступом в Java, чтобы ограничить доступ к своим внутренним данным: поле CONTEXT имеет модификатор доступа private, что позволяет фреймворку разделять информацию внутри себя между двумя своими методами. Эта информация недоступна пользовательскому коду и скрыта от него. Мы говорим, что объект ScopedValue является объектом-полномочием (capability), который даёт коду, имеющему право к нему обращаться, возможность привязывать или читать значение. Часто ScopedValue будет иметь доступ private, но иногда может иметь доступ protected или пакетный доступ, чтобы несколько взаимодействующих классов могли читать и привязывать значение.
Повторная привязка значений Scoped Values
То, что у значений Scoped Values нет метода set(), означает, что вызывающий метод может использовать значение Scoped Values, чтобы надёжно передать значение вызываемым методам в том же потоке. Однако бывают случаи, когда одному из вызываемых методов нужно использовать то же значение Scoped Values, чтобы передать другое значение своим собственным вызываемым методам. API ScopedValue позволяет установить новую вложенную привязку для последующих вызовов:
private static final ScopedValue<String> X = ScopedValue.newInstance();
void foo() {
ScopedValue.where(X, "hello").run(() -> bar());
}
void bar() {
System.out.println(X.get()); // prints hello
ScopedValue.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 в пользовательском коде — API Structured Concurrency (структурированная конкурентность; JEP 453), а именно класс StructuredTaskScope. Значения Scoped Values родительского потока автоматически наследуются дочерними потоками, созданными с помощью StructuredTaskScope. Код в дочернем потоке может использовать привязки, установленные для значения Scoped Values в родительском потоке, с минимальными накладными расходами. В отличие от локальных переменных потока, привязки значений Scoped Values родительского потока не копируются в дочерний поток.
Ниже приведён пример наследования значений Scoped Values, происходящего незаметно в пользовательском коде. Метод Server.serve привязывает CONTEXT и вызывает Application.handle, как и раньше. Однако пользовательский код в Application.handle выполняет методы readUserInfo() и fetchOffers() конкурентно, каждый в собственном потоке Virtual Threads, с помощью StructuredTaskScope.fork (1, 2). Каждый метод может использовать Framework.readKey, который, как и раньше, обращается к значению Scoped Values CONTEXT (4). Другие подробности пользовательского кода здесь не рассматриваются; дополнительную информацию см. в JEP 453.
@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 Values 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.where(...).run(...). Principal остаётся в области видимости, пока выполняется дочерний поток, а scope.join() гарантирует, что дочерние потоки завершатся до того, как run сможет вернуть управление, уничтожив привязку. Так удаётся избежать проблемы неограниченного времени жизни, возникающей при использовании локальных переменных потока. Унаследованные классы управления потоками, такие как ForkJoinPool, не поддерживают наследование значений Scoped Values, поскольку не могут гарантировать, что дочерний поток, порождённый в области видимости какого-либо родительского потока, завершится до того, как родительский поток покинет эту область.
Переход на значения Scoped Values
Значения Scoped Values, вероятно, окажутся полезными и предпочтительными во многих сценариях, где сегодня используются локальные переменные потока. Помимо роли скрытых параметров методов, значения Scoped Values могут помочь в следующем:
-
Реентерабельный код — иногда желательно обнаруживать рекурсию, например потому, что фреймворк не является реентерабельным, или потому, что рекурсию нужно как-то ограничить. Значение Scoped Values даёт способ это сделать: настройте его как обычно, с помощью
ScopedValue.whereиrun, а затем глубоко в стеке вызовов вызовитеScopedValue.isBound(), чтобы проверить, есть ли у него привязка для текущего потока. В более сложном варианте значение Scoped Values может моделировать счётчик рекурсии за счёт многократной повторной привязки. -
Вложенные транзакции — обнаружение рекурсии также может быть полезно в случае плоских транзакций: любая транзакция, начатая во время выполнения другой транзакции, становится частью самой внешней транзакции.
-
Графические контексты — другой пример встречается в графике, где часто есть контекст рисования, который нужно разделять между частями программы. Значения Scoped Values благодаря автоматической очистке и реентерабельности подходят для этого лучше, чем локальные переменные потока.
В целом мы рекомендуем переходить на значения Scoped Values, когда назначение локальной переменной потока совпадает с целью значения Scoped Values: односторонней передачей неизменяемых данных. Если в кодовой базе локальные переменные потока используются двусторонним образом — когда вызываемый метод глубоко в стеке вызовов передаёт данные удалённому вызывающему методу через ThreadLocal.set, — или совершенно неструктурированно, то переход невозможен.
Есть несколько сценариев, в которых предпочтительны локальные переменные потока. Пример — кэширование объектов, которые дорого создавать и использовать, таких как экземпляры java.text.DateFormat. Общеизвестно, что экземпляр java.text.SimpleDateFormat изменяем, поэтому его нельзя разделять между потоками без синхронизации. Выделение каждому потоку собственного объекта SimpleDateFormat через локальную переменную потока, существующую в течение всего времени жизни потока, часто было практичным подходом. Однако сегодня любой код, кэширующий SimpleDateFormat, может перейти на DateTimeFormatter, поскольку его можно хранить в поле static final и разделять между потоками.
API ScopedValue
Полный API ScopedValue богаче, чем небольшое подмножество, описанное выше. Хотя в этом JEP приведены только примеры с ScopedValue<V>.where(V, <value>).run(aRunnable), привязать значение Scoped Values можно и другими способами. Например, API также предоставляет версию Callable, которая возвращает значение и может также выбросить Exception:
try {
var result = ScopedValue.where(X, "hello").call(() -> bar());
catch (Exception e) {
handleFailure(e);
}
...
Кроме того, существуют сокращённые версии методов привязки. Например, ScopedValue<V>.runWhere(V, <value>, aRunnable) — краткая форма ScopedValue<V>.where(V, <value>).run(aRunnable). Хотя эта краткая форма иногда удобна, она позволяет привязать только одно значение Scoped Values за раз.
Полный API значений Scoped Values можно найти здесь
Альтернативы
Многие возможности значений Scoped Values можно эмулировать с помощью локальных переменных потока, хотя и ценой некоторых потерь в объёме занимаемой памяти, безопасности и производительности.
Мы экспериментировали с изменённой версией ThreadLocal, которая поддерживает некоторые характеристики значений Scoped Values. Однако дополнительный груз локальных переменных потока приводит к чрезмерно громоздкой реализации, или к API, который для значительной части основной функциональности возвращает UnsupportedOperationException, или к тому и другому одновременно. Поэтому лучше не изменять ThreadLocal, а ввести значения Scoped Values как совершенно отдельное понятие.
Значения Scoped Values созданы под влиянием того, как многие диалекты Lisp поддерживают свободные переменные с динамической областью видимости, в частности того, как такие переменные ведут себя в многопоточной среде выполнения с глубоким связыванием, такой как Interlisp-D. Scoped Values улучшают свободные переменные Lisp, добавляя типобезопасность, неизменяемость, инкапсуляцию и эффективный доступ внутри потоков и между ними.