Графический анализ принципов и сценариев применения ThreadLocal

Java

Введение в ThreadLocal

Класс ThreadLocal должен быть всем знаком, и он напрямую переводится как线程本地(变量), мы часто используем его, чтобы сохранить некоторыенить изолирована,Глобальныйпеременная информация. При использовании ThreadLocal для поддержки переменных каждый поток получает эксклюзивную копию переменной для этого потока.
ThreadLocal больше похож на копию подземелья в DNF, а каждый поток похож на каждого игрока, входящего в копию DNF. После того, как каждый поток входит в копию, он относительно изолирован и не будет мешать друг другу, что очень удобно в некоторых сценариях многопоточности.龙龙的奇妙比喻--ThreadLocal

Жизненный цикл ThreadLocal между глобальными переменными и локальными переменными

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

  • глобальная переменная

статическая переменнаяstatic Object val = new Object()или переменная-член объектаObject val = new Object(), первый хранится в области методов JVM, второй хранится в области кучи и восстанавливается вместе с GC объекта,Все потоки могут получить доступ

  • локальная временная переменная

Объекты, объявленные и созданные в методе или блоке кода, также хранятся в области кучи и освобождаются, поскольку в конце блока кода нет ссылки.Эксклюзивный поток, но время жизни существует только в этом блоке метода

  • Переменные ThreadLocal

private static ThreadLocal<Object> store = new ThreadLocal<>();,Потоки являются эксклюзивными и могут быть получены на любом этапе выполнения потока..

Общий сценарий использования Threadlocal

Сценарии, в которых я часто использую ThreadLocal:

  • Идентификатор requestId (или traceId) запроса микрослужбы — это глобально уникальный UUID, используемый для уникальной маркировки запроса в модуле микрослужбы. Один и тот же идентификатор трассировки, передаваемый в разных модулях, инкапсулируется и сериализуется/десериализуется инфраструктурой RPC. После того, как инфраструктура RPC десериализует traceid, он положит его в ThreadLocal, чтобы запрос можно было идентифицировать по ID при логировании лога на каждом этапе работы модуля. А с завершением микросервисной инфраструктуры traceid также можно последовательно подключить через сервис для анализа состояния и времени, затрачиваемого на каждом этапе запроса.

requestid

  • Как локальный кеш db, через трехуровневую связь ThreadLocal-redis-MySQL, ThreadLocal, как кеш с кратчайшим жизненным циклом, кэширует результаты запроса и может быстро возвращать тот же запрос в том же потоке.

Принцип реализации ThreadLocal

ThreadLocal.get()

Структура реализации ThreadLocal и процесс выполнения показаны на следующем рисунке.Несколько ключевых слов для ThreadLocal.

  1. Хэшкаждая нить независимо поддерживает ThreadLocalmap, что являетсяEntry[]Массив, получить индекс Entry путем хеширования ThreadLocal (подробности читатели могут узнать из исходного кода)
  2. Решение хэш-коллизий используетзакон об открытых адресах, в случае конфликта хэшей, как показано на рисунке, переместите нижний индекс в одно место и найдите его снова (три решения конфликта хэшей: метод застежки-молнии, метод открытого адреса и метод повторного хеширования, обычно используемые в HashMap, заинтересованные читатели могут искать сами по себе: разрешение конфликтов хэшей). ThreadLocal обычно не хранит большой объем данных, а использование метода открытого адреса (или метода открытой адресации) экономит место для хранения указателей по сравнению с методом zipper
  3. Слабая ссылка WeakReference, ссылка на ThreadLocal в ThreadLocalMap использует слабую ссылку. Функция слабой ссылки состоит в том, чтобы не препятствовать восстановлению GC, когда ссылка является единственной ссылкой объекта. Далее будет обсуждаться проблема слабой ссылки и утечки памяти в ThreadLocal .

Слабые ссылки и внимание к использованию в ThreadLocalMap

Как упоминалось выше, ThreadLocalMap на самом деле представляет собой сопоставление значений ThreadLocal --> Конкретные отношения реализации следующие:ThreaLocal清理的过程Когда для ThreadLocal, используемого в потоке, установлено значение null, слабая ссылка в ThreadLocalMap используется как последняя ссылка на ThreadLocal и напрямую повторно используется при возникновении GC, но значение в Entry не будет повторно использоваться в это время. .
В методе set/get/remove ThreadLocal, когда он встречает узел с ключом == null (называемый устаревшим гнилым узлом), он выполняет логику обработки, такую ​​как очистка.

  1. Если Thread1 будет выполнен и уничтожен, то ThreadLocalMap будет полностью уничтожен, и проблемы с утечкой памяти не будет.
  2. Если Thread1 существует в течение длительного времени и создает новый ThreadLocal и никогда не выполнял метод set/get/remove, это может привести к утечке памяти.
  3. Обычно мы будем использоватьПул потоков, это будет выглядеть как конец потока после выполнения, на самом деле поток простоСнова в пуле и жду следующего расписанияВ этом случае ThreadLocal будет повторно использоваться.Если мы используем ThreadLocal для сохранения traceId в предыдущем сценарии использования, если поток не перезапускается после выполнения и traceId не сбрасывается при следующем выполнении, то при печати логаОн снова напечатает предыдущий traceId, что также приведет к множеству логических ошибок

Следовательно, вы должны использовать нить выполнения после вызова, наконец, ThreadLocalthreadLocal.remove(), или еслиThreadLocal<HashMap>тогда позвониthreadlocal.get().remove()Очистить хэш-карту

Репликация ThreadLocal

При использовании ThreadLocal нам часто нужно создавать дочерние потоки, надеясь, что дочерний поток может наследовать ThreadLocal родительского потока, или взять в качестве примера сценарий использования traceid, мы создаем дочерние потоки для одновременной обработки трудоемкой логики , и надеемся, что дочерний поток также сможет точно распечатать идентификатор трассировки текущего запроса, ноОбычная информация ThreadLocal полностью теряется после создания нового потока, я наступил на яму здесь.

Так что просто решение скопировать Threadlocal в подпоток:

  1. Сначала сохраните содержимое ThreadLocal в куче, а затем скопируйте содержимое кучи в дочерний поток


2. InheritableThreadLocal (пул потоков недействителен), принцип заключается в том, что дочерний поток создается вызовом метода new Thread() в родительском потоке, а метод Thread#init вызывается в конструкторе Thread. Скопируйте данные родительского потока в дочерний поток в методе init. ноУведомление! Обычно мы используем пул потоков для создания новых потоков.В этом случае так называемое создание новых потоков только повторно использует существующие потоки в пуле потоков и не вызывает метод new Thread(), поэтому использование InheritableThreadLocal часто неэффективно. .
3. Alibaba имеет TransmittableThreadLocal с открытым исходным кодом, который, как говорят, решает проблему в 2. Мы можем рассмотреть это позже.Я обычно использую только метод 1, чтобы в основном решить проблему репликации ThreadLocal подпотока.

reference

[1] Конфликт ThreadLocal-хэша и утечка памяти
[2] Стратегия интервью ThreadLocal: понять каждую деталь и принцип ее построения
[3] Интервьюер: Мальчик, я слышал, что вы видели исходный код ThreadLocal? (Углубленный анализ ThreadLocal с 4D-графикой)