Gymterview
middle

Что такое 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 создаёт серьёзные проблемы:

  1. Потребление памяти — при миллионах виртуальных потоков каждый ThreadLocal создаёт запись в хэш-таблице потока, что может привести к OutOfMemoryError.
  2. Утечки памяти — если забыть вызвать remove(), значение хранится до завершения потока. В пуле Platform Threads поток переиспользуется и значение «перетекает» в следующую задачу. С Virtual Threads (которые одноразовые) утечка другого рода — огромное количество живых записей.
  3. InheritableThreadLocal — при использовании с пулом потоков значение копируется при создании потока, но потоки переиспользуются, и следующая задача видит «чужое» значение.

Рекомендация: для новых проектов на Java 21+ вместо ThreadLocal используйте ScopedValue (см. соответствующий вопрос) — он иммутабелен, автоматически очищается и оптимизирован для Virtual Threads.

Аналогия: представьте офис с общими шкафчиками. У каждого сотрудника (потока) есть свой ящик с одним и тем же номером (один ThreadLocal), но содержимое внутри — индивидуальное. Уборщица не знает, кому принадлежит ящик, поэтому если сотрудник уволился (поток завершился), а ящик не очищен — получается утечка.

На собеседовании могут спросить: «Где в реальных фреймворках используется ThreadLocal?» Хороший ответ: Spring хранит транзакционный контекст (TransactionSynchronizationManager), Security-контекст (SecurityContextHolder) и RequestAttributes через ThreadLocal. Именно поэтому при переходе на Virtual Threads и Structured Concurrency эти компоненты обновляются для поддержки ScopedValue.