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).