Что такое ThreadLocal-переменная?
ThreadLocal — это класс из пакета java.lang, который позволяет создать переменную, имеющую отдельное, независимое значение для каждого потока. Иными словами, один и тот же объект ThreadLocal хранит разные данные в зависимости от того, из какого потока к нему обращаются.
Как устроено внутри. У каждого экземпляра Thread существует скрытое поле threadLocals — это хэш-таблица типа ThreadLocal.ThreadLocalMap. Ключом служит ссылка на объект ThreadLocal, значением — объект, «положенный» в эту переменную вызовом set(). Когда вы вызываете get(), класс ThreadLocal заглядывает в таблицу текущего потока (Thread.currentThread()) и возвращает значение, ассоциированное с данным ключом.
Пример
// Объявляем ThreadLocal-переменную
ThreadLocal<String> userContext = new ThreadLocal<>();
// В потоке A
userContext.set("admin");
System.out.println(userContext.get()); // "admin"
// В потоке B (одновременно)
userContext.set("guest");
System.out.println(userContext.get()); // "guest"
// Поток A по-прежнему видит "admin"
Инициализация через withInitial. Начиная с Java 8, удобнее задавать значение по умолчанию фабричным методом:
Пример
ThreadLocal<SimpleDateFormat> dateFormat =
ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd"));
Теперь каждый поток при первом вызове get() автоматически получит собственный экземпляр SimpleDateFormat, что решает проблему потоконебезопасности этого класса.
Типичные сценарии использования:
| Сценарий | Пример |
|---|---|
| Хранение контекста запроса | Идентификатор пользователя, request ID для логирования |
| Потоконебезопасные объекты | SimpleDateFormat, NumberFormat, Random |
| Соединения с БД | Одно соединение на поток в пуле |
| Транзакционный контекст | Spring @Transactional хранит контекст в ThreadLocal |
Важно: изоляция касается ссылок, а не самих объектов. Если два потока через свои ThreadLocal-слоты ссылаются на один и тот же мутабельный объект, конкурентные модификации возможны. ThreadLocal изолирует именно ссылки.
Инициализация в правильном потоке. Распространённая ошибка — вызвать set() в главном потоке, а затем ожидать, что значение будет доступно в рабочем потоке. Этого не произойдёт: set() привязывает значение к потоку, в котором вызван, поэтому рабочий поток при вызове get() получит null (или значение withInitial).
Обязательная очистка. После окончания работы необходимо вызывать remove(), особенно при использовании пулов потоков. Без этого значение «перетекает» в следующую задачу, исполняемую тем же потоком из пула:
Пример
ThreadLocal<String> requestId = new ThreadLocal<>();
void handleRequest(String id) {
requestId.set(id);
try {
processRequest();
} finally {
requestId.remove(); // обязательно!
}
}
Проблемы ThreadLocal с Virtual Threads (Java 21+):
С появлением виртуальных потоков ThreadLocal создаёт серьёзные проблемы:
- Потребление памяти — при миллионах виртуальных потоков каждый
ThreadLocalсоздаёт запись в хэш-таблице потока, что может привести кOutOfMemoryError. - Утечки памяти — если забыть вызвать
remove(), значение хранится до завершения потока. В пуле Platform Threads поток переиспользуется и значение «перетекает» в следующую задачу. С Virtual Threads (которые одноразовые) утечка другого рода — огромное количество живых записей. InheritableThreadLocal— при использовании с пулом потоков значение копируется при создании потока, но потоки переиспользуются, и следующая задача видит «чужое» значение.
Рекомендация: для новых проектов на Java 21+ вместо ThreadLocal используйте ScopedValue (см. соответствующий вопрос) — он иммутабелен, автоматически очищается и оптимизирован для Virtual Threads.
Аналогия: представьте офис с общими шкафчиками. У каждого сотрудника (потока) есть свой ящик с одним и тем же номером (один
ThreadLocal), но содержимое внутри — индивидуальное. Уборщица не знает, кому принадлежит ящик, поэтому если сотрудник уволился (поток завершился), а ящик не очищен — получается утечка.
На собеседовании могут спросить: «Где в реальных фреймворках используется
ThreadLocal?» Хороший ответ: Spring хранит транзакционный контекст (TransactionSynchronizationManager), Security-контекст (SecurityContextHolder) и RequestAttributes черезThreadLocal. Именно поэтому при переходе на Virtual Threads и Structured Concurrency эти компоненты обновляются для поддержкиScopedValue.