ThreadLocal — довольно часто используемый служебный класс из пакета java.lang в JDK, реализующий контекст потока. Вероятно, самое распространённое его применение — хранение пользовательского контекста после входа в систему и другой ключевой информации.
Контекст потока также позволяет элегантно решать многие на первый взгляд сложные задачи, например интеграцию фреймворков spring-mybatis:
При разработке на основе экосистемы spring мы обычно поручаем spring управление транзакциями, а с помощью аннотации transactional настраиваем их включение, уровень изоляции и другие параметры. К базе данных мы тоже обращаемся не напрямую через jdbc, а через фреймворк mybatis. spring и mybatis — два совершенно независимых фреймворка, однако при их использовании кажется, что mybatis может сразу получить и использовать соединение connection с базой данных, открытое spring. Нам практически ничего не нужно делать: достаточно подключить jar-пакет mybatis-spring-boot-stater.
Ключ к реализации этого изящного решения — ThreadLocal. В общих чертах принцип таков: пакет stater jar в конечном счёте подключит для нас MybatisAutoConfiguration и внедрить через SPI в контейнер bean-компонентов нашего сервиса, в MybatisAutoConfiguration уже настроено в mybatis необходимые SqlSessionFactory,а также внедрили собственный компонент spring SpringManagedTransactionFactory(реализует TransactionFactory,фабрика транзакций — предоставляемый mybatis механизм расширения, позволяющий делегировать управление транзакциями).
Общий процесс выполнения таков: метод с аннотацией transactional в итоге оборачивается прокси AOP. В предварительном уведомлении класс прокси получает connection из dataSource и затем помещает его в ThreadLocal (на самом деле в ThreadLocal хранится map, где key — dataSource, а value — connectionHolder, эквивалентный connection). При последующем выполнении SQL в mybatis сначала через TransactionFactory Получение connection, поскольку внедрённый на этот момент TransactionFactory Да SpringManagedTransactionFactory,поэтому соответствующее dataSource соединение connection извлекается из ThreadLocal.
Краткое объяснение принципа работы ThreadLocal:
ThreadLocalMap представляет собой хеш-таблицу, в которой коллизии разрешаются методом линейного пробирования; для каждого потока создаётся свой экземпляр, а в каждом объекте ThreadLocal есть один threadLocalHashCode Используется для вычисления хеш-ключа.
Сравним с HashMap,ThreadLocalMap Выбор линейного пробирования вместо метода цепочек для разрешения коллизий может объясняться следующими соображениями:
1、Соображения расхода памяти: из-за различий в сценариях использования объём данных в ThreadLocalMap обычно значительно меньше, чем в HashMap, а при небольшом объёме данных метод цепочек требует много дополнительной памяти для указателей
2、Эффективность: на основе пункта 1, при небольшом количестве элементов сэкономленную память можно использовать для создания хеш-таблицы с меньшим коэффициентом заполнения, что уменьшит число коллизий и повысит среднюю скорость поиска

Проблема
A В задачах на основе пулов потоков возникает проблема передачи контекста, особенно в модели веб-программирования на базе Tomcat:поскольку сам Tomcat использует пул потоков для обработки взаимодействия запрос–ответ между сервером и клиентом, каждому веб-приложению приходится вручную обеспечивать корректную передачу контекста потока。
B Проблема производительности: если в приложении слишком много ThreadLocal, хеш-таблица разрастается, и скорость поиска снижается
Для проблемы A обычно используют примерно один и тот же подход; просто приведу код из нашей системы
У этого решения есть две проблемы:
1、Если впоследствии появится новый ThreadLocal, который необходимо корректно передавать, придётся изменить код и добавить его в логику wrap. Любой пропуск приведёт к ошибке
2、В некоторых случаях задачу может выполнить сам поток, который её отправил, например:
Когда пул потоков заполнен и его
RejectedExecutionHandlerИспользуетсяCallerRunsPolicyзадачи, отправленные в пул потоков, выполняются непосредственно в отправившем их потоке, и в таком случае завершающая логика очистки приводит к потере контекста этого потокаАналогично, используя
ForkJoinPool(включая параллельное выполнениеStreamиCompletableFuture,в основе используетсяForkJoinPool)сценария, развёрнутыйForkJoinTaskбудет выполняться непосредственно в потоке, отправляющем задачу. Завершающая очистка также приведёт к потере контекста этого потока
Решение:
Проблема 1:если бы класс ThreadLocal предоставлял API,позволяющий узнать,сколько всего объектов ThreadLocal сейчас существует в приложении,проблема решилась бы сама собой,но,к сожалению,такого API нет。Если всё же добиваться этого,можно через рефлексию получить из объекта thread inheritableThreadLocals(то есть один ThreadLocalMap)свойство, а затем обойти хеш-таблицу и извлечь все записи entry, чтобы получить все пары ключей и значений ThreadLocal. Другой подход — создать дополнительную обёртку над ThreadLocal из JDK и добавить в неё описанные выше возможности.
Проблема 2:её можно решить, доработав логику wrap:при запуске потока сначала получить и временно сохранить контекст текущего потока, затем заменить его контекстом потока, отправившего задачу. Перед завершением выполнения потока контекст следует не очищать, а восстанавливать из временно сохранённых данных.
transmittable-thread-local
Готовое решение уже существует: принцип его работы опирается на два описанных выше направления, а самому процессу дали название CRR(capture/replay/restore) режим。
Есть три способа использования:
1 ### Модификация Runnable и Callable
TransmittableThreadLocal<String> context = new TransmittableThreadLocal<>();
// =====================================================
// 在父线程中设置
context.set("value-set-in-parent");
Runnable task = new RunnableTask();
// 额外的处理,生成修饰了的对象ttl
Runnable Runnable ttlRunnable = TtlRunnable.get(task);
executorService.submit(ttlRunnable);
// =====================================================
// Task中可以读取,值是"value-set-in-parent"
String value = context.get();2 Модификация пула потоков
ExecutorService executorService = ...
// 额外的处理,生成修饰了的对象
executorService executorService = TtlExecutors.getTtlExecutorService(executorService);
TransmittableThreadLocal<String> context = new TransmittableThreadLocal<>();
// =====================================================
// 在父线程中设置
context.set("value-set-in-parent");
Runnable task = new RunnableTask();
Callable call = new CallableTask();
executorService.submit(task);
executorService.submit(call);
// =====================================================
// Task或是Call中可以读取,值是"value-set-in-parent"
String value = context.get();3 ### Использование Java Agent использовать для модификации JDK Класс реализации пула потоков
// ## 1. 框架上层逻辑,后续流程框架调用业务 ##
TransmittableThreadLocal<String> context = new TransmittableThreadLocal<>();
context.set("value-set-in-parent");
// ## 2. 应用逻辑,后续流程业务调用框架下层逻辑 ##
ExecutorService executorService = Executors.newFixedThreadPool(3);
Runnable task = new RunnableTask();
Callable call = new CallableTask();
executorService.submit(task);
executorService.submit(call);
// ## 3. 框架下层逻辑 ##
// Task或是Call中可以读取,值是"value-set-in-parent"
String value = context.get();Преимущество использования Java Agent заключается в том, что не нужно вмешиваться в код приложения. А главным недостатком, на мой взгляд, является то, что, если при запуске JVM забыть добавить параметры, это может привести к сбою.
Кратко рассмотрим логику исходного кода
Что касается вопроса B, при разработке приложений его обычно не рассматривают, поскольку требования к скорости выполнения не настолько высоки. Однако при разработке некоторых низкоуровневых компонентов промежуточного ПО оптимизации производительности могут уделять больше внимания. Например, netty предпочёл самостоятельно реализовать FastThreadLocal заменяет ThreadLocal из jdk и одновременно предоставляет сопутствующий FastThreadLocalThread,в FastThreadLocalThread и FastThreadLocal При совместном использовании можно всегда обеспечивать временную сложность поиска O(1). Принцип решения — обмен памяти на время: чтобы коллизии в хеш-таблице не снижали эффективность поиска, создаётся достаточно длинный массив, каждый FastThreadLocal при создании каждому выделяется уникальный индекс массива — словно у каждого своё место; при поиске достаточно взять свой index и напрямую получить элемент массива.
Приведу небольшой фрагмент исходного кода