Введение в ThreadLocal
Класс ThreadLocal должен быть всем знаком, и он напрямую переводится как线程本地(变量)
, мы часто используем его, чтобы сохранить некоторыенить изолирована,Глобальныйпеременная информация. При использовании ThreadLocal для поддержки переменных каждый поток получает эксклюзивную копию переменной для этого потока.
ThreadLocal больше похож на копию подземелья в DNF, а каждый поток похож на каждого игрока, входящего в копию DNF. После того, как каждый поток входит в копию, он относительно изолирован и не будет мешать друг другу, что очень удобно в некоторых сценариях многопоточности.
Жизненный цикл 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 также можно последовательно подключить через сервис для анализа состояния и времени, затрачиваемого на каждом этапе запроса.
- Как локальный кеш db, через трехуровневую связь ThreadLocal-redis-MySQL, ThreadLocal, как кеш с кратчайшим жизненным циклом, кэширует результаты запроса и может быстро возвращать тот же запрос в том же потоке.
Принцип реализации ThreadLocal
ThreadLocal.get()
Структура реализации ThreadLocal и процесс выполнения показаны на следующем рисунке.Несколько ключевых слов для ThreadLocal.
-
Хэшкаждая нить независимо поддерживает ThreadLocalmap, что является
Entry[]
Массив, получить индекс Entry путем хеширования ThreadLocal (подробности читатели могут узнать из исходного кода) - Решение хэш-коллизий используетзакон об открытых адресах, в случае конфликта хэшей, как показано на рисунке, переместите нижний индекс в одно место и найдите его снова (три решения конфликта хэшей: метод застежки-молнии, метод открытого адреса и метод повторного хеширования, обычно используемые в HashMap, заинтересованные читатели могут искать сами по себе: разрешение конфликтов хэшей). ThreadLocal обычно не хранит большой объем данных, а использование метода открытого адреса (или метода открытой адресации) экономит место для хранения указателей по сравнению с методом zipper
- Слабая ссылка WeakReference, ссылка на ThreadLocal в ThreadLocalMap использует слабую ссылку. Функция слабой ссылки состоит в том, чтобы не препятствовать восстановлению GC, когда ссылка является единственной ссылкой объекта. Далее будет обсуждаться проблема слабой ссылки и утечки памяти в ThreadLocal .
Слабые ссылки и внимание к использованию в ThreadLocalMap
Как упоминалось выше, ThreadLocalMap на самом деле представляет собой сопоставление значений ThreadLocal --> Конкретные отношения реализации следующие:Когда для ThreadLocal, используемого в потоке, установлено значение null, слабая ссылка в ThreadLocalMap используется как последняя ссылка на ThreadLocal и напрямую повторно используется при возникновении GC, но значение в Entry не будет повторно использоваться в это время. .
В методе set/get/remove ThreadLocal, когда он встречает узел с ключом == null (называемый устаревшим гнилым узлом), он выполняет логику обработки, такую как очистка.
- Если Thread1 будет выполнен и уничтожен, то ThreadLocalMap будет полностью уничтожен, и проблемы с утечкой памяти не будет.
- Если Thread1 существует в течение длительного времени и создает новый ThreadLocal и никогда не выполнял метод set/get/remove, это может привести к утечке памяти.
- Обычно мы будем использоватьПул потоков, это будет выглядеть как конец потока после выполнения, на самом деле поток простоСнова в пуле и жду следующего расписанияВ этом случае ThreadLocal будет повторно использоваться.Если мы используем ThreadLocal для сохранения traceId в предыдущем сценарии использования, если поток не перезапускается после выполнения и traceId не сбрасывается при следующем выполнении, то при печати логаОн снова напечатает предыдущий traceId, что также приведет к множеству логических ошибок
Следовательно, вы должны использовать нить выполнения после вызова, наконец, ThreadLocalthreadLocal.remove()
, или еслиThreadLocal<HashMap>
тогда позвониthreadlocal.get().remove()
Очистить хэш-карту
Репликация ThreadLocal
При использовании ThreadLocal нам часто нужно создавать дочерние потоки, надеясь, что дочерний поток может наследовать ThreadLocal родительского потока, или взять в качестве примера сценарий использования traceid, мы создаем дочерние потоки для одновременной обработки трудоемкой логики , и надеемся, что дочерний поток также сможет точно распечатать идентификатор трассировки текущего запроса, ноОбычная информация ThreadLocal полностью теряется после создания нового потока, я наступил на яму здесь.
Так что просто решение скопировать Threadlocal в подпоток:
- Сначала сохраните содержимое 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-графикой)