Можно ли создать ThreadLocal для несущего потока виртуального потока Java?
JEP-425: Virtual Threads заявляет, что новый виртуальный поток «следует создавать для каждой задачи приложения», и дважды ссылается на возможность запуска «миллионов» виртуальных потоков в JVM.
Тот же JEP подразумевает, что каждый виртуальный поток будет иметь доступ к своему локальному значению потока:
Виртуальные потоки поддерживают локальные переменные потока [...] точно так же, как потоки платформы, поэтому они могут запускать существующий код, использующий локальные переменные потока.
Локальные переменные потока часто используются для кэширования объекта, который не является потокобезопасным и требует больших затрат на создание. JEP предупреждает:
Однако, поскольку виртуальных потоков может быть очень много, используйте локальные потоки после тщательного рассмотрения.
Действительно многочисленные! Особенно с учетом того, что виртуальные потоки не объединены в пул (или, по крайней мере, не должны быть). Как представитель недолговечной задачи, использование локальных переменных потока в виртуальном потоке с целью кэширования дорогостоящего объекта кажется лишенным смысла. Пока не! Мы можем из виртуального потока создавать и получать доступ к локальным элементам потока, привязанным к его потоку-носителю.
Для пояснения, я хотел бы пойти от чего-то вроде этого (что было бы вполне приемлемо при использовании только собственных потоков, ограниченных размером пула, но это явно больше не очень эффективный механизм кэширования при непрерывном запуске миллионов виртуальных потоков воссоздано:
static final ThreadLocal<DateFormat> CACHED = ThreadLocal.withInitial(DateFormat::getInstance);
К этому (увы, этот класс не является частью общедоступного API):
static final ThreadLocal<DateFormat> CACHED = new jdk.internal.misc.CarrierThreadLocal();
// CACHED.set(...)
Еще до того, как мы туда доберемся. Следует спросить, это безопасная практика?
Ну, насколько я правильно понял виртуальные потоки, это просто логические этапы, выполняемые в потоке платформы (также известном как «поток-носитель») с возможностью размонтирования вместо ожидания блокировки. Итак, я предполагаю - пожалуйста, поправьте меня, если я ошибаюсь - что 1) виртуальный поток никогда не будет чередоваться с другим виртуальным потоком в том же потоке носителя или перепланирован в другом потоке носителя, если только код не был бы заблокирован в противном случае и, следовательно, если 2 ) операция, которую мы вызываем для кэшированного объекта, никогда не блокируется, тогда задача/виртуальный поток будет просто выполняться от начала до конца на одном и том же носителе, и поэтому да, было бы безопасно кэшировать объект на локальном потоке платформы.
Рискуя ответить на мой собственный вопрос, JEP-425 указывает, что это невозможно:
Локальные переменные потока носителя недоступны для виртуального потока, и наоборот.
Мне не удалось найти общедоступный API для получения потока-носителя или явного выделения локальных переменных потока в потоке платформы [из виртуального потока], но это не значит, что мое скромное исследование является окончательным. Может есть способ?
Затем я прочитал JEP-429: Scoped Values , который на первый взгляд кажется ударом богов Java, чтобы избавиться отThreadLocal
вообще или, по крайней мере, предоставить альтернативу виртуальным потокам. На самом деле JEP использует многословие, такое как «переход на значения с ограниченной областью действия», и говорит, что они «предпочтительнее локальных переменных потока, особенно при использовании большого количества виртуальных потоков».
Для всех вариантов использования, обсуждаемых в JEP, я могу только согласиться. Но ближе к концу этого документа мы также находим это:
Есть несколько сценариев, в которых предпочтение отдается локальным переменным потока. Примером может служить кэширование объектов, создание и использование которых требует больших затрат, таких как экземпляры java.text.DateFormat. Общеизвестно, что объект DateFormat является изменяемым, поэтому его нельзя использовать совместно между потоками без синхронизации. Предоставление каждому потоку собственного объекта DateFormat через локальную переменную потока, которая сохраняется в течение всего времени существования потока, часто является практичным подходом.
В свете того, что обсуждалось ранее, использование thread-local может быть «практичным», но не идеальным. Фактически, сам JEP-429 фактически начинался с очень говорящего замечания: «если каждый из миллиона виртуальных потоков имеет изменяемые локальные переменные потока, объем памяти может быть значительным».
Обобщить:
Нашли ли вы способ выделить локальные элементы потока в потоке-носителе из виртуального потока?
Если нет, можно ли с уверенностью сказать, что для приложений, использующих виртуальные потоки, практика кэширования объектов в локальном потоке мертва, и нужно будет реализовать/использовать другой подход, такой как параллельный кеш/карта/пул/что-то еще?
1 ответ
Вы написали
Итак, я предполагаю (поправьте меня, если я ошибаюсь), что
- виртуальный поток никогда не будет чередоваться другим виртуальным потоком в том же потоке несущей или перепланироваться в другом потоке несущей, если в противном случае код не был бы заблокирован и, следовательно, если
- операция, которую мы вызываем для кэшированного объекта, никогда не блокируется, тогда задача/виртуальный поток будет просто выполняться от начала до конца на одном и том же носителе, и поэтому да, было бы безопасно кэшировать объект в локальном потоке платформы.
Но в документе State of Loom говорится:
Вы не должны делать никаких предположений о том, где находятся точки планирования, в отличие от сегодняшних потоков. Даже без принудительного вытеснения любой вызов JDK или библиотечного метода может привести к блокировке и, следовательно, к точке переключения задач.
и далее :
С этой целью мы планируем, чтобы виртуальная машина поддерживала операцию, которая пытается принудительно прервать выполнение в любой безопасной точке. Как эта возможность будет доступна планировщикам, пока неизвестно, и, скорее всего, она не попадет в первую предварительную версию.
Так
Предположение о том, что виртуальный поток освобождает поток-носитель только тогда, когда он вот-вот будет заблокирован, применимо только к текущему предварительному просмотру. Упреждающее переключение между виртуальными потоками разрешено и даже запланировано на будущее.
Даже если мы предположим, что виртуальный поток может освободить поток-носитель только при выполнении операций блокировки, мы не сможем предсказать, когда может произойти операция блокировки.
Одним из примеров операций, находящихся вне нашего контроля, является загрузка классов . Загрузка данных класса является блокирующей операцией, а загрузка классов для обычных JVM реализуется лениво. Возможно даже, что метод, который вызывался несколько раз, внезапно выполняет необычный путь, использующий класс, который ранее не использовался.
Другой пример — загрузка ресурсов . Даже такой простой пример, как ваш, уже включает в себя ресурсы, организованные неопределенным образом, например, данные о часовых поясах или локализованные названия месяцев и дней недели.
Таким образом, нет никакого способа иметь безопасно работающий локальный кэш оператора, и ваше предположение о том, что использование локальных потоков потоков (или подобных) для кэширования мертво, действительно верно. Вместо этого вы можете использовать пул объектов, но, поскольку это подразумевает некоторую синхронизацию, вы могли бы также рассмотреть возможность использования просто одного пула объектов.DateFormat
¹ и синхронизироваться по нему. Это позволит реализовать вашу первоначальную идею не освобождать поток-носитель во время использования объекта.
Конечно, в этом конкретном примере лучшим вариантом будет использованиеDateTimeFormatter
изjava.time
API, который является потокобезопасным и, следовательно, позволяет использовать один экземпляр всем потокам.
¹ или один из нескольких, выбранный способом, не требующим синхронизации