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

JEP 446: Scoped Values (Preview)

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

АвторAndrew Haley & Andrew Dinn
ОтветственныйAndrew Haley
ТипFeature
ОбластьSE
СтатусClosed / Delivered
Выпуск21
Компонентcore-libs
Обсуждениеloom dash dev at openjdk dot org
Связан сJEP 429: Scoped Values (Incubator)
JEP 464: Scoped Values (Second Preview)
РецензентыAlan Bateman, Mark Reinhold
ОдобренBrian Goetz
Создан2023/03/16 16:01
Обновлён2026/07/24 14:14
Задача8304357

Аннотация

Вводятся Scoped Values — значения, которые можно безопасно и эффективно передавать методам без использования параметров методов. Они предпочтительнее thread-local-переменных, особенно при использовании большого числа потоков Virtual Threads (виртуальные потоки). Это API в статусе Preview.

По сути, scoped-значение — это неявный параметр метода. Всё происходит «так, как если бы» у каждого метода в последовательности вызовов был дополнительный невидимый параметр. Ни один из методов не объявляет этот параметр, и только методы, у которых есть доступ к объекту scoped-значения, могут получить его значение (данные). Scoped Values позволяют безопасно передавать данные от вызывающего метода к далёкому вызываемому методу через последовательность промежуточных методов, которые не объявляют параметр для этих данных и не имеют к ним доступа.

История

Scoped Values были в статусе Incubator (инкубационный модуль) в JDK 20 (JEP 429). В JDK 21 эта возможность больше не находится в статусе Incubator, а является API в статусе Preview.

Цели

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

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

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

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

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

  • Цель не состоит в том, чтобы изменить язык программирования Java.

  • Цель не состоит в том, чтобы требовать отказа от thread-local-переменных или пометить существующий 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, содержащий идентификатор аутентифицированного пользователя, идентификатор транзакции и т. д., и связывать его с текущей транзакцией. Все операции фреймворка используют объект контекста, но пользовательский код его не использует (и для этого кода он не важен).

По сути, фреймворк хочет передать свой внутренний контекст из своего метода 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-переменная объявляется как поле final static, чтобы к ней было легко обратиться из разных методов, и 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 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);
    }
}

Благодаря 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-переменных.

К облегчённому совместному использованию данных

Проблемы thread-local-переменных стали острее с появлением Virtual Threads (JEP 425). 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 могут быть thread-local-переменные; более того, короткое время жизни потоков Virtual Threads и то, что они не объединяются в пулы, делают упомянутую выше проблему долговременных утечек памяти менее острой. (Вызывать метод remove() thread-local-переменной не нужно, если поток быстро завершается, поскольку при завершении его thread-local-переменные удаляются автоматически.) Однако если у каждого из миллиона потоков Virtual Threads есть собственная копия thread-local-переменных, потребление памяти может быть значительным.

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

Описание

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

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

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

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

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

// In a method called directly or indirectly from the lambda expression
... V.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

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

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 возвращает управление, bar видит привязку "hello". Тело bar не может изменить привязку, которую видит сам этот метод, но может изменить привязку, которую видят вызываемые им методы. Это гарантирует ограниченное время жизни для совместного использования нового значения.

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

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

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

Предпочтительный механизм создания потоков Virtual Threads в пользовательском коде — API Structured Concurrency (структурированная конкурентность) (JEP 428), а именно класс StructuredTaskScope. Значения Scoped Values родительского потока автоматически наследуются дочерними потоками, созданными с помощью StructuredTaskScope. Код в дочернем потоке может использовать привязки, установленные для значения Scoped Values в родительском потоке, с минимальными накладными расходами. В отличие от thread-local-переменных, привязки значений 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 428.

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

Переход на Scoped Values

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

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

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

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

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

Есть несколько сценариев, в которых предпочтительнее thread-local-переменные. Например, кэширование объектов, которые дорого создавать и использовать, таких как экземпляры java.text.DateFormat. Как известно, объект DateFormat изменяемый, поэтому его нельзя совместно использовать между потоками без синхронизации. Предоставить каждому потоку собственный объект DateFormat через thread-local-переменную, которая существует в течение всего времени жизни потока, часто оказывается практичным подходом.

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

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

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

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