Как работает класс ThreadLocal и каковы его сценарии использования?
ThreadLocal хранит отдельное значение для каждого потока. Используется для per-thread контекста (транзакции, request-ID, соединения с БД). Обязателен вызов remove() после завершения, иначе в пулах потоков происходит утечка памяти.
Как работает ThreadLocal
ThreadLocal<T> не хранит значения сам — каждый Thread содержит внутри себя экземпляр ThreadLocalMap. Ключ в этой карте — ThreadLocal-объект (через WeakReference), значение — ваши данные. Поэтому вызов get() из разных потоков возвращает разные объекты.
public class RequestContext {
private static final ThreadLocal<String> REQUEST_ID =
ThreadLocal.withInitial(() -> java.util.UUID.randomUUID().toString());
public static String getRequestId() {
return REQUEST_ID.get();
}
public static void setRequestId(String id) {
REQUEST_ID.set(id);
}
public static void clear() {
REQUEST_ID.remove(); // ОБЯЗАТЕЛЬНО
}
}
Сценарии использования
- Транзакционный контекст: привязка
Connectionк потоку в SpringTransactionSynchronizationManager. - Request ID / MDC: Logback/Log4j2 используют
MDC(Mapped Diagnostic Context) — обёртку над ThreadLocal для трассировки. - SimpleDateFormat: не потокобезопасен; хранится в ThreadLocal для переиспользования без синхронизации (устаревший паттерн — сейчас лучше
DateTimeFormatter). - SecurityContext: Spring Security хранит
AuthenticationвSecurityContextHolder, который по умолчанию используетMODE_THREADLOCAL.
Правильное использование с пулом потоков
import java.util.concurrent.*;
ExecutorService pool = Executors.newFixedThreadPool(10);
pool.submit(() -> {
RequestContext.setRequestId("req-123");
try {
processRequest();
} finally {
RequestContext.clear(); // очищаем перед возвратом потока в пул
}
});
InheritableThreadLocal
InheritableThreadLocal копирует значения из родительского потока в дочерний при его создании. Но с пулами потоков это не работает — потоки переиспользуются и не создаются заново для каждой задачи.
private static final InheritableThreadLocal<String> TENANT_ID =
new InheritableThreadLocal<>();
// В родительском потоке
TENANT_ID.set("company-42");
// Новый дочерний поток автоматически получит "company-42"
new Thread(() -> System.out.println(TENANT_ID.get())).start();
ThreadLocal vs Scoped Values (Java 21+)
В современном коде с виртуальными потоками (Project Loom) рекомендуется использовать ScopedValue — он неизменяем, автоматически очищается, имеет лучшую производительность при большом числе потоков.
Подводные камни
- Утечка памяти в пулах: если не вызвать
remove(), значение остаётся вThreadLocalMapпотока навсегда. Поток из пула обработает тысячи запросов, накапливая объекты. - Слабые ключи и значения: ключ (ThreadLocal) хранится как WeakReference — при сборке ключа запись в ThreadLocalMap становится «осиротевшей» (stale entry). Очистка происходит только при следующих операциях с картой, а не сразу.
- InheritableThreadLocal и пулы: в ExecutorService потоки не создаются заново — наследование не срабатывает. Для передачи контекста в задачи используйте обёртку типа
TaskDecoratorв Spring или явную передачу параметром. - Отладка: утечки ThreadLocal сложно обнаруживаются в heap dump — ищите
java.lang.ThreadLocalMap$Entry[]в анализаторах типа Eclipse MAT. - Виртуальные потоки:
ThreadLocalработает с виртуальными потоками, но при миллионах потоков создаёт миллионы ThreadLocalMap. Используйте Scoped Values для новых приложений на Java 21+.
Common mistakes
- Путать термин «threadlocal» с соседним механизмом Java.
- Не называть границу lifecycle, transaction, thread или request для «threadlocal».
- Игнорировать production-эффекты «threadlocal»: latency, SQL shape, memory, security или observability.
What the interviewer is testing
- Попросить объяснить механизм «threadlocal» на минимальном примере.
- Проверить, видит ли кандидат failure mode и диагностику для «threadlocal».
- Уточнить, какие настройки или API меняют «threadlocal» в реальном сервисе.