Почему ThreadLocal течёт в пуле потоков

Вопрос проверяет, понимаете ли вы жизненный цикл ThreadLocal и его связь с пулами потоков. В обычном коде поток создаётся и умирает, ThreadLocalMap умирает вместе с ним. В пуле поток живёт годами, и ThreadLocalMap живёт вместе с ним.

Каждая запись в ThreadLocalMap держит слабую ссылку на сам объект ThreadLocal, но сильную — на значение. Если после использования не вызвать remove(), значение остаётся привязанным к потоку до его завершения или до перезаписи тем же ключом.

private static final ThreadLocal FORMATTER = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd"));

public void process() { try { // работа с FORMATTER.get() } finally { FORMATTER.remove(); } }

В Tomcat и большинстве HTTP-серверов потоки из пула переживают тысячи запросов — забытый ThreadLocal накапливает объекты, которые никто не читает, но GC не может собрать.

Спросят, чем это отличается от утечки через static-поле — по механике идентично: сильная ссылка держит объект живым дольше, чем нужно. Разница в том, что ThreadLocal ловится реже, потому что каждый поток видит своё значение и баг не воспроизводится в однопоточном тесте.

remove() в finally — не защита от ошибок, а обязательная часть контракта ThreadLocal в пуле потоков.

Тренажёр: 600 вопросов, мок с таймером, план повторов

senior·base — что спрашивают на самом деле


В этом посте были ссылки, но мы их удалили по правилам Сетки