В чём опасность ThreadLocal в пуле потоков?
Поток в пуле переиспользуется: значение ThreadLocal, не очищенное после задачи, протекает в следующую задачу (чужой пользователь, транзакция) и удерживает память. Правило: try { set } finally { remove() }.
- Утечка сильнее в серверах приложений — держится загрузчик классов при передеплое
- Фреймворки (Spring Security, MDC логов) очищают сами — свой код обязан тоже
- Контекст не переходит в другие потоки: CompletableFuture и пулы его теряют
А ещё С виртуальными потоками — ScopedValue вместо ThreadLocal
Подробно
ThreadLocal хранит значение для каждого потока отдельно — удобно для контекста запроса: текущий пользователь, id трассировки, соединение.
Проблема в пуле. Сервер обслуживает запросы пулом из, например, 200 потоков. Запрос A кладёт в ThreadLocal пользователя и не очищает. Поток возвращается в пул и берёт запрос B — тот видит пользователя A. Это не только ошибка, но и уязвимость.
Утечка памяти. Значение живёт, пока жив поток, а потоки пула живут вечно. Большие объекты в ThreadLocal накапливаются. В серверах приложений ещё хуже: значение ссылается на класс веб-приложения, и при передеплое старый загрузчик классов не может быть собран.
Правильно:
CONTEXT.set(user);
try {
handle(request);
} finally {
CONTEXT.remove();
}
Передача в другие потоки. Значение не переходит в задачу, отправленную в пул или в стадию CompletableFuture — контекст переносят явно (обёртка над Runnable, TaskDecorator в Spring).
Виртуальные потоки: их миллионы, и ThreadLocal на каждом дорог; рекомендуемая замена — ScopedValue (Java 25), значение, привязанное к области выполнения.
Что хотят услышать
- Протекание контекста между задачами.
- Утечка памяти из-за долгоживущих потоков.
- remove в finally.
- Потеря контекста при переходе в другой поток.
Частые ошибки
- Забыть remove.
- Ожидать, что значение видно в задаче из пула.
Проверьте себя
Один верный вариант. Подумайте, потом откройте ответ.
Показать варианты и ответ
- ✗ThreadLocal вообще не работает в пулах
- ✗ThreadLocal замедляет поток
- ✓Поток переиспользуется, и значение протекает в следующую задачу — нужен remove()
- ✗Значение очищается автоматически после задачи, поэтому данные между задачами теряются
Что спросят дальше
- Как Spring хранит transaction context?
- Почему MDC логгера чистят после запроса?
Разбор темы
- ThreadLocal и Fork/Join
Состояние на поток и утечки в пулах, InheritableThreadLocal, work-stealing в ForkJoinPool и общий пул parallel streams.
Связанные вопросы
Разбор собеседования с sotto
После собеседования sotto разбирает разговор: оценка, каждый вопрос — хорошо, частично или слабо, чего не хватило в ответе и над чем поработать к следующему разу. Первые 30 минут бесплатно.