Получить доступ

ThreadLocal и Fork/Join

Состояние на поток и утечки в пулах, InheritableThreadLocal, work-stealing в ForkJoinPool и общий пул parallel streams.

ThreadLocal: состояние на поток

ThreadLocal даёт каждому потоку собственную копию значения — это способ хранить контекст без передачи его параметрами через все вызовы. Типичные применения: request/user context, MDC логгера, transaction context.

Технически значения лежат в ThreadLocalMap внутри самого объекта Thread, а ThreadLocal — лишь ключ доступа. Поэтому время жизни значения привязано к времени жизни потока.

Утечки в пулах

Главная опасность — пулы потоков: поток живёт долго и переиспользуется между запросами, поэтому положенное в ThreadLocal значение переживёт запрос и «протечёт» в следующий, а также не даст собраться объектам, на которые ссылается (утечка памяти).

Правило: в пуле ThreadLocal обязательно очищают через remove() в finally по завершении единицы работы. Ключи в ThreadLocalMap слабые, но значения — сильные, поэтому без remove утечка реальна.

try {
    CTX.set(context);
    handle(request);
} finally {
    CTX.remove(); // обязательно в пуле потоков
}

InheritableThreadLocal

InheritableThreadLocal копирует значение в дочерний поток в момент его создания. Это удобно для проброса контекста в порождённые потоки, но в пулах работает не так, как ждут: рабочие потоки создаются заранее и не «наследуют» контекст текущего запроса.

Поэтому для проброса контекста в асинхронные задачи обычно используют явную передачу или специальные обёртки (context propagation), а не InheritableThreadLocal.

ForkJoinPool и work-stealing

ForkJoinPool заточен под divide-and-conquer: задача рекурсивно делится на подзадачи (fork) и потом собирает результаты (join). Реализуют это через RecursiveTask (с результатом) или RecursiveAction.

Его сила — work-stealing: у каждого потока своя двусторонняя очередь задач, и простаивающий поток «крадёт» задачи с хвоста очереди занятого. Это балансирует нагрузку без центрального диспетчера и хорошо масштабируется на CPU-bound задачах.

Общий пул и parallel streams

parallelStream() и многие async-операции по умолчанию работают на общем ForkJoinPool.commonPool, размер которого — число ядер минус один. Это значит: общий ресурс на весь процесс, и блокирующая операция в parallelStream «затыкает» его для всех.

Вывод: parallel streams хороши для CPU-bound обработки больших объёмов в памяти и плохи для блокирующего IO. Для своего параллелизма с контролем создают отдельный ForkJoinPool.

Как отвечать на собеседовании

Объясните ThreadLocal как per-thread состояние (ThreadLocalMap в Thread) и обязательный remove() в пулах (иначе утечка/протечка контекста), оговорите подвох InheritableThreadLocal в пулах. Для Fork/Join назовите work-stealing и предупредите про общий commonPool у parallel streams (не блокировать, CPU-bound).

Вопросы по теме

Читайте также

Закрытая бета

Знать тему — половина дела

На живом собеседовании sotto слышит вопрос интервьюера и показывает короткий ответ и план. Оставьте контакт — пришлём доступ и промокод.

  • Промокод на 2 часа — сверх пробных 30 минут
  • Программа для Windows — скачать сразу, войти — через Яндекс ID или VK ID
  • Прямая связь с разработчиком: что улучшить — сделаем