ThreadLocal

ThreadLocal<T> дает каждому потоку отдельное значение для одного ключа:

private static final ThreadLocal<RequestContext> CONTEXT =
        ThreadLocal.withInitial(RequestContext::new);
 
void handle(Request request) {
    try {
        CONTEXT.set(contextFrom(request));
        process(request);
    } finally {
        CONTEXT.remove();
    }
}

Значение хранится не «внутри ThreadLocal», а в структуре, связанной с текущим Thread. Ключи там удерживаются слабыми ссылками, значения — нет. Если забыть remove() в долгоживущем thread pool, следующий запрос может увидеть старый контекст, а объект способен оставаться в памяти до очистки stale entry или завершения потока.

Подходящие случаи

  • request/correlation context в синхронной цепочке;
  • не-thread-safe объект, который оправданно переиспользуется одним потоком;
  • технический контекст, который невозможно разумно передавать параметром.

Явный параметр обычно прозрачнее: зависимости видны в сигнатуре и не привязаны к модели потоков.

Границы

  • значение не передается автоматически в задачи executor;
  • один запрос может продолжиться на другом потоке;
  • один поток пула обслуживает много запросов;
  • InheritableThreadLocal копирует значение при создании дочернего потока, поэтому с заранее созданными пулами не решает propagation.

В реактивной цепочке или при асинхронных стадиях нужен контекст соответствующего фреймворка либо явная передача.

На собеседовании

  • Почему remove() обязателен в пуле? Поток живет дольше запроса, иначе утечка данных и памяти.
  • Делает ли ThreadLocal общий объект thread-safe? Нет; он должен хранить отдельный объект на поток.
  • Переносится ли значение в executor? Обычно нет.
  • Где физически хранится значение? В ThreadLocalMap текущего Thread.
  • Почему слабого ключа недостаточно против утечки? Значение остается сильно достижимым через map до очистки записи.