Статьи

Когда и как использовать ThreadLocal

Как уже могли догадаться наши читатели, я ежедневно сталкиваюсь с утечками памяти. Особый тип сообщений OutOfMemoryError недавно начал привлекать мое внимание — проблемы, вызванные неправильным использованием ThreadLocals, становятся все более и более частыми. Глядя на причины таких утечек, я начинаю полагать, что более половины из них вызваны разработчиками, которые либо не имеют ни малейшего представления о том, что они делают, либо пытаются решить проблемы, которые не предназначены для их решения. ,

Вместо того, чтобы стиснуть зубы, я решил открыть тему, опубликовав две статьи, первую из которых вы сейчас читаете. В этом посте я объясняю мотивацию использования ThreadLocal . Во втором посте, который находится в процессе разработки, я открою капот ThreadLocal и посмотрю на реализацию.

Давайте начнем с воображаемого сценария, в котором использование ThreadLocal действительно разумно. За это, скажите привет нашему гипотетическому разработчику по имени Тим. Тим разрабатывает веб-приложение, в котором много локализованного контента. Например, пользователь из Калифорнии ожидал, что его встретят с датой, отформатированной с использованием знакомого шаблона MM / dd / yy , а другой из Эстонии, с другой стороны, хотел бы видеть дату, отформатированную в соответствии с dd.MM.yyyy. Поэтому Тим начинает писать код так:

1
2
3
4
5
6
7
8
9
public String formatCurrentDate() {
        DateFormat df = new SimpleDateFormat("MM/dd/yy");
        return df.format(new Date());
    }
 
    public String formatFirstOfJanyary1970() {
        DateFormat df = new SimpleDateFormat("MM/dd/yy");
        return df.format(new Date(0));
    }

Через некоторое время Тим находит это скучным и противоречит передовой практике — код приложения загрязнен такими инициализациями. Поэтому он делает, казалось бы, разумный шаг, извлекая DateFormat в переменную экземпляра. После этого его код выглядит следующим образом:

1
2
3
4
5
6
7
8
9
private DateFormat df = new SimpleDateFormat("MM/dd/yy");
 
    public String formatCurrentDate() {
        return df.format(new Date());
    }
 
    public String formatFirstOfJanyary1970() {
        return df.format(new Date(0));
    }

Удовлетворенный результатами рефакторинга, Тим подбрасывает себе воображаемую пятерку, отправляет изменения в хранилище и идет домой. Несколько дней спустя пользователи начинают жаловаться — некоторые из них, похоже, получают полностью искаженные строки вместо прежних красиво отформатированных дат.

Исследуя проблему, Тим обнаруживает, что реализация DateFormat не является поточно- ориентированной . Это означает, что в приведенном выше сценарии, если два потока одновременно используют методы formatCurrentDate () и formatFirstOfJanyary1970 () , существует вероятность того, что состояние искажается, и отображаемый результат может быть испорчен. Таким образом, Тим решает проблему, ограничивая доступ к методам, чтобы убедиться, что один поток за раз входит в функциональность форматирования. Теперь его код выглядит следующим образом:

1
2
3
4
5
6
7
8
9
private DateFormat df = new SimpleDateFormat("MM/dd/yy");
 
    public synchronized String formatCurrentDate() {
        return df.format(new Date());
    }
 
    public synchronized String formatFirstOfJanyary1970() {
        return df.format(new Date(0));
    }

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

Читая дальше о проблеме, Тим обнаруживает переменные другого типа, называемые ThreadLocal . Эти переменные отличаются от своих обычных аналогов тем, что каждый поток, который обращается к одному (через метод get или set ThreadLocal), имеет свою собственную, независимо инициализированную копию переменной. Довольный недавно обнаруженной концепцией, Тим снова переписывает код:

01
02
03
04
05
06
07
08
09
10
11
12
13
public static ThreadLocal df = new ThreadLocal() {
        protected DateFormat initialValue() {
            return new SimpleDateFormat("MM/dd/yy");
        }
    };
 
    public String formatCurrentDate() {
        return df.get().format(new Date());
    }
 
    public String formatFirstOfJanyary1970() {
        return df.get().format(new Date(0));
    }

Проходя через такой процесс, Тим с помощью болезненных уроков усвоил мощную концепцию. Применяемый, как в последнем примере, результат служит хорошим примером преимуществ.

Но недавно найденная концепция опасна. Если Тим использовал один из классов приложения вместо связанных с JDK классов DateFormat, загружаемых загрузчиком классов начальной загрузки, мы уже находимся в опасной зоне. Если вы просто забудете удалить его после выполнения поставленной задачи, копия этого объекта останется в потоке, который, как правило, принадлежит пулу потоков. Поскольку срок службы пула потоков превышает срок службы приложения, он предотвращает сборку мусора объектом и, следовательно, ClassLoader, отвечающим за загрузку приложения. И мы создали утечку, которая может появиться в старой доброй java.lang.OutOfMemoryError: Космическая форма PermGen

Еще один способ начать злоупотреблять этой концепцией — использовать ThreadLocal в качестве хака для получения глобального контекста в вашем приложении. Спуск по этой кроличьей норе — это верный способ исказить код вашего приложения со всеми видами невообразимых зависимостей, связывающих всю вашу кодовую базу в неразрывный беспорядок.

Ссылка: когда и как использовать ThreadLocal от нашего партнера по JCG Никиты Сальникова Тарновского в блоге Plumbr Blog .