Перейти к основному содержимому

Поиск по CooLWJ

选择搜索范围,输入关键词后按回车。

Язык

Контекст потока Java

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:

tapd_50394669_1660562598_27.png

ThreadLocalMap представляет собой хеш-таблицу, в которой коллизии разрешаются методом линейного пробирования; для каждого потока создаётся свой экземпляр, а в каждом объекте ThreadLocal есть один threadLocalHashCode Используется для вычисления хеш-ключа.
Сравним с HashMap,ThreadLocalMap Выбор линейного пробирования вместо метода цепочек для разрешения коллизий может объясняться следующими соображениями:
1、Соображения расхода памяти: из-за различий в сценариях использования объём данных в ThreadLocalMap обычно значительно меньше, чем в HashMap, а при небольшом объёме данных метод цепочек требует много дополнительной памяти для указателей
2、Эффективность: на основе пункта 1, при небольшом количестве элементов сэкономленную память можно использовать для создания хеш-таблицы с меньшим коэффициентом заполнения, что уменьшит число коллизий и повысит среднюю скорость поиска

Схема связей между ThreadLocal, потоком и ThreadLocalMap

Проблема
A В задачах на основе пулов потоков возникает проблема передачи контекста, особенно в модели веб-программирования на базе Tomcat:поскольку сам Tomcat использует пул потоков для обработки взаимодействия запрос–ответ между сервером и клиентом, каждому веб-приложению приходится вручную обеспечивать корректную передачу контекста потока。
B Проблема производительности: если в приложении слишком много ThreadLocal, хеш-таблица разрастается, и скорость поиска снижается

Для проблемы A обычно используют примерно один и тот же подход; просто приведу код из нашей системы
2.png
У этого решения есть две проблемы:
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 забыть добавить параметры, это может привести к сбою.

Кратко рассмотрим логику исходного кода
3.png

4.png 

Что касается вопроса B, при разработке приложений его обычно не рассматривают, поскольку требования к скорости выполнения не настолько высоки. Однако при разработке некоторых низкоуровневых компонентов промежуточного ПО оптимизации производительности могут уделять больше внимания. Например, netty предпочёл самостоятельно реализовать FastThreadLocal заменяет ThreadLocal из jdk и одновременно предоставляет сопутствующий FastThreadLocalThread,в FastThreadLocalThread и FastThreadLocal При совместном использовании можно всегда обеспечивать временную сложность поиска O(1). Принцип решения — обмен памяти на время: чтобы коллизии в хеш-таблице не снижали эффективность поиска, создаётся достаточно длинный массив, каждый FastThreadLocal при создании каждому выделяется уникальный индекс массива — словно у каждого своё место; при поиске достаточно взять свой index и напрямую получить элемент массива.
Приведу небольшой фрагмент исходного кода
5.png


Комментарии

Отправка

Выберите оформление

Выбранное оформление применяется ко всему сайту.