Статьи

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

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

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

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

	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 в переменную экземпляра. После этого его код выглядит следующим образом:

	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 () , существует вероятность того, что состояние искажается, и отображаемый результат может быть испорчен. Таким образом, Тим решает проблему, ограничивая доступ к методам, чтобы убедиться, что один поток за раз входит в функции форматирования. Теперь его код выглядит следующим образом:

	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), имеет свою собственную, независимо инициализированную копию переменной. Довольный недавно обнаруженной концепцией, Тим снова переписывает код:

	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 в качестве хака для получения глобального контекста в вашем приложении. Спуск по этой кроличьей норе — это верный способ исказить код вашего приложения со всеми видами невообразимых зависимостей, связывающих всю вашу кодовую базу в неразрывный беспорядок.

Я надеюсь, что первая часть истории уже дала пищу для размышлений. Чтобы быть в числе читателей второй части истории, не забудьте подписаться на нашу ленту RSS или Twitter .