В многопоточном приложении потоки разделяют общую память. Это создаёт проблемы с состоянием. ThreadLocal решает это элегантно — каждый поток получает свою копию переменной.
🔹 Как устроено внутри
Никакой центральной Map на уровне JVM нет. Всё проще: у каждого объекта Thread есть поле threadLocals типа ThreadLocal.ThreadLocalMap. Это внутренняя Map, которая живёт внутри самого потока.
Когда ты вызываешь set() — значение кладётся в Map текущего потока, где ключ — сам объект ThreadLocal:
// упрощённо, внутри ThreadLocal.set(value)
Thread.currentThread().threadLocals.put(this, value);
get() — достаёт из Map того же потока. Другой поток обращается к своей Map и видит своё значение. Пересечений нет по конструкции.
🔹 Инициализация через withInitial
Если get() вызван, а set() ещё не было, возвращается null. Чтобы этого избежать, используют фабрику:
ThreadLocal<SimpleDateFormat> sdf =
ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd"));
Лямбда вызовется один раз на поток, при первом get(). Дальше кешируется в той же threadLocals Map.
🔹 Что реально использует ThreadLocal в экосистеме
Spring держит через него всё, что должно быть доступно в рамках одного запроса без явной передачи через параметры:
→ TransactionSynchronizationManager — текущее соединение с БД, чтобы сервис и репозиторий в рамках @Transactional работали с одним коннектом
→ SecurityContextHolder — аутентифицированный пользователь
→ RequestContextHolder — текущий HttpServletRequest
Идея одна: вместо того чтобы тащить контекст через каждый метод, кладёшь его в ThreadLocal на входе в поток и достаёшь где нужно.
🔹 Где ломается
Thread Pool. Потоки переиспользуются и после обработки запроса поток возвращается в пул, но его threadLocals Map никуда не девается. Если не вызвать remove(), то следующий запрос, попавший в этот поток, найдёт чужие данные.
Это не гипотетическая проблема. Именно так утекают security-контексты и появляются баги, которые воспроизводятся только под нагрузкой.
Второй момент — утечка памяти. ThreadLocalMap использует WeakReference на ключ, но значение держится сильной ссылкой. Если поток живёт долго (а в пуле — фактически вечно) и remove() никто не вызвал — объект не будет собран GC, пока поток жив.
Поэтому паттерн всегда один:
try {
tl.set(value);
// работа
} finally {
tl.remove();
}ThreadLocal решает конкретную задачу — изоляция состояния без синхронизации. Не серебряная пуля, не замена synchronized. Просто инструмент для случаев, когда данные должны жить в рамках потока, а не шариться между ними.
══════ Навигация ══════
Вакансии • Задачи • Собесы
🐸 Библиотека джависта
#CoreJava