Подтвердить что ты не робот

Действительно ли это моя работа по очистке ресурсов ThreadLocal, когда классы были открыты для пула потоков?

Мое использование ThreadLocal

В моих классах Java я иногда использую ThreadLocal главным образом как средство избежать ненужного создания объекта:

@net.jcip.annotations.ThreadSafe
public class DateSensitiveThing {

    private final Date then;

    public DateSensitiveThing(Date then) {
        this.then = then;
    }

    private static final ThreadLocal<Calendar> threadCal = new ThreadLocal<Calendar>()   {
        @Override
        protected Calendar initialValue() {
            return new GregorianCalendar();
        }
    };

    public Date doCalc(int n) {
        Calendar c = threadCal.get();
        c.setTime(this.then):
        // use n to mutate c
        return c.getTime();
    }
}

Я делаю это по уважительной причине - GregorianCalendar - это один из тех славных, изменчивых, изменяемых, не-потоковых объектов, который предоставляет услугу для нескольких вызовов, а не представляет значение. Кроме того, считается, что это "дорого" для создания экземпляра (верно ли это или нет, это не вопрос этого вопроса). (В целом, я действительно восхищаюсь этим: -))

Как Tomcat Whinges

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

На самом деле, Tomcat 7 так бежит, когда останавливается webapp:

SEVERE: веб-приложение [] создало ThreadLocal с ключом типа [org.apache.xmlbeans.impl.store.CharUtil $1] (значение [[email protected]]) и значение type [java.lang.ref.SoftReference] (значение [[email protected]]), но не удалось удалить его, когда веб-приложение было остановлено. Темы будут обновлены время, чтобы попытаться избежать вероятной утечки памяти. 13 декабря 2012 г. 12:54:30 org.apache.catalina.loader.WebappClassLoader checkThreadLocalMapForLeaks

(Даже мой код не делает этого в этом конкретном случае).

Кто виноват?

Это вряд ли справедливо. Tomcat обвиняет меня (или пользователя моего класса) за правильную работу.

В конечном счете, это потому, что Tomcat хочет повторно использовать потоки, которые он мне предложил, для других веб-приложений. (Ugh - я чувствую себя грязным.) Вероятно, это не отличная политика в отношении Tomcat - потому что потоки фактически имеют/вызывают состояние - не делятся между ними.

Однако эта политика, по крайней мере, распространена, даже если это нежелательно. Я чувствую, что я обязан - как пользователь ThreadLocal предоставить способ для моего класса "освободить" ресурсы, которые мой класс подключил к различным потокам.

Но что с этим делать?

Какая правильная вещь здесь?

Мне кажется, что политика перенаправления потоков сервлет-движка не согласуется с намерением ThreadLocal.

Но, может быть, я должен предоставить средство, позволяющее пользователям сказать "begone, evil thread-specific state, связанное с этим классом, хотя я не в состоянии позволить потоку умереть и позволить GC сделать свое дело?". Возможно ли мне это сделать? Я имею в виду, что я не могу организовать ThreadLocal#remove() для вызова каждой из нитей, которые видели в прошлом ThreadLocal#initialValue(). Или есть другой способ?

Или я должен просто сказать моим пользователям: "Идите и получите себе достойную реализацию загрузчика классов и потоков"?

EDIT # 1: Уточнено, как threadCal используется в классе утилиты vanailla, который не знает о жизненных циклах потоков EDIT # 2: Исправлена ​​проблема безопасности потока в DateSensitiveThing

4b9b3361

Ответ 1

Вздох, это старые новости

Хорошо, немного поздно к вечеринке на этом. В октябре 2007 года Джош Блох (соавтор java.lang.ThreadLocal вместе с Дугом Ли) написал:

"Использование пулов потоков требует особой осторожности. Небрежное использование потока бассейны в сочетании с неаккуратным использованием локаторов потоков могут непреднамеренное удержание объекта, как было отмечено во многих местах".

Люди жаловались на плохое взаимодействие ThreadLocal с пулами потоков даже тогда. Но Джош санкционировал:

"Примеры экземпляров для производительности. Пример Aaron SimpleDateFormat (см. выше) является одним из примеров этого шаблона.

Некоторые уроки

  • Если вы помещаете какие-либо объекты в любой пул объектов, вы должны предоставить способ удалить их "позже".
  • Если вы используете пул ThreadLocal, у вас есть ограниченные возможности для этого. Или: a) вы знаете, что Thread (s), где вы положили значения, прекратится, когда ваше приложение будет завершено; ИЛИ б) вы можете позже организовать тот же поток, который вызывается ThreadLocal # set() для вызова ThreadLocal # remove(), когда ваше приложение завершает
  • Таким образом, использование ThreadLocal в качестве пула объектов будет определять высокую стоимость дизайна вашего приложения и вашего класса. Преимущества не предоставляются бесплатно.
  • Таким образом, использование ThreadLocal, вероятно, является преждевременной оптимизацией, хотя Джошуа Блох призвал вас рассмотреть его в "Эффективной Java".

Короче говоря, решение использовать ThreadLocal в качестве формы быстрого бесконтактного доступа к "пулам экземпляров потоков" не является решением, которое нужно воспринимать легкомысленно.

ПРИМЕЧАНИЕ. Существуют и другие виды использования ThreadLocal, отличные от "пулов объектов", и эти уроки не применяются к тем сценариям, где ThreadLocal предназначен только для временного размещения, или когда имеется подлинная сквозная нить чтобы отслеживать.

Последствия для разработчиков библиотек

Вот некоторые последствия для разработчиков библиотек (даже если такие библиотеки являются простыми служебными классами в вашем проекте).

Или:

  • Вы используете ThreadLocal, полностью осознавая, что вы можете "загрязнять" длительные потоки дополнительным багажом. Если вы выполняете java.util.concurrent.ThreadLocalRandom, это может быть уместно. (Tomcat может все еще болтать у пользователей вашей библиотеки, если вы не используете в java.*). Интересно отметить дисциплину, с помощью которой java.* делает щадящее использование техники ThreadLocal.

ИЛИ

  1. Вы используете ThreadLocal и предоставляете клиентам ваш класс/пакет: a) возможность выбрать, чтобы отказаться от этой оптимизации ( "не использовать ThreadLocal... я не могу организовать очистку" ); А ТАКЖЕ b) способ очистки ресурсов ThreadLocal ("ОК, чтобы использовать ThreadLocal... Я могу организовать для всех потоков, которые вы использовали для вызова LibClass.releaseThreadLocalsForThread(), когда я закончил с ними.

Делает вашу библиотеку "трудной для использования".

ИЛИ

  1. Вы даете своим клиентам возможность предоставить свое собственное имплантирование объекта-пула (которое может использовать ThreadLocal или какую-то синхронизацию). ( "Хорошо, я могу дать вам new ExpensiveObjectFactory<T>() { public T get() {...} }, если вы считаете, что это действительно необязательно".

Не так уж плохо. Если объект действительно так важен и что его сложно создать, возможно, явный пул.

ИЛИ

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

Альтернативы

  • Регулярный пул объектов со всей его согласованной синхронизацией.
  • Не объединяйте объекты - просто создайте их в локальной области и отпустите позже.
  • Не объединять потоки (если вы не можете запланировать код очистки, если хотите) - не используйте ваши вещи в контейнере JaveEE
  • Пулы потоков, которые достаточно умны, чтобы очистить ThreadLocals без зависания у вас.
  • Пулы потоков, которые выделяют потоки по принципу "для каждого приложения", а затем позволяют им умереть, когда приложение остановлено.
  • Протокол между контейнерами объединения потоков и приложениями, которые разрешили регистрацию "обработчика выключения приложения", который контейнер мог планировать для выполнения на потоках, которые были использованы для обслуживания приложения... в какой-то момент в будущем, когда эта тема была доступна. Например. servletContext.addThreadCleanupHandler(new Handler() {@Override cleanup() {...}})

Было бы неплохо увидеть некоторую стандартизацию вокруг трех последних элементов в будущих спецификациях JavaEE.

Bootnote

Собственно, создание a GregorianCalendar довольно легкое. Это неизбежный вызов setTime(), который несет большую часть работы. Он также не имеет значительного состояния между различными точками выработки потока. Помещение Calendar в ThreadLocal вряд ли даст вам больше, чем вам стоит... если профилирование определенно не показывает горячую точку в new GregorianCalendar().

new SimpleDateFormat(String) является дорогостоящим для сравнения, поскольку он должен разбирать строку формата. После синтаксического анализа "состояние" объекта имеет важное значение для последующего использования одним и тем же потоком. Это лучше подходит. Но это может быть "менее дорого" для создания нового, чем предоставление вашим классам дополнительных обязанностей.

Ответ 2

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

ADD: Способ использования подготовленного GregorianCalendar просто неверен: поскольку запросы службы могут быть параллельными, а синхронизации нет, doCalc может принимать getTime ater setTime, вызванный другим запросом. Введение синхронизации будет замедлять работу, так что создание нового GregorianCalendar может быть лучшим вариантом.

Другими словами, ваш вопрос должен состоять в следующем: как сохранить пул готовых экземпляров GregorianCalendar, чтобы его число было настроено для запроса скорости. Итак, как минимум, вам нужен синглтон, который содержит этот пул. Каждый контейнер Ioc имеет средства для управления синглом, и большинство из них имеют готовые реализации пула объектов. Если вы еще не используете контейнер IoC, начните использовать один (String, Guice), а не изобретать колесо.

Ответ 3

Если какая-либо помощь я использую пользовательский SPI (интерфейс) и JDK ServiceLoader. Затем все мои внутренние библиотеки (банки), которые должны выполнять разгрузку threadlocals, просто следуют шаблону ServiceLoader. Поэтому, если банку нужна очистка потока, она будет автоматически выбрана, если у нее есть соответствующий /META-INF/services/interface.name.

Затем я выполняю разгрузку в фильтре или слушателе (у меня были некоторые проблемы с слушателями, но я не могу вспомнить, что).

Было бы идеально, если бы JDK/JEE поставлялся с стандартным SPI для очистки threadlocals.

Ответ 4

Я думаю, что JDK ThreadPoolExecutor смог очистить ThreadLocals после выполнения задачи, но, как мы знаем, это не так. Я думаю, он мог бы предоставить хотя бы вариант. Причина может быть вызвана тем, что Thread предоставляет только закрытый доступ к своим картам TreadLocal, поэтому ThreadPoolExecutor просто не может получить к ним доступ, не изменяя API Thread.

Интересно, что ThreadPoolExecutor защитил дескрипторы методов beforeExecution и afterExecution, API говорит: These can be used to manipulate the execution environment; for example, reinitializing ThreadLocals.... Поэтому я могу представить задачу, которая реализует интерфейс ThreadLocalCleaner и наш пользовательский ThreadPoolExecutor, который на afterExecution вызывает задачу cleanThreadLocals();

Ответ 5

Подумав об этом в течение года, я решил, что неприменимо для контейнера JavaEE для совместного использования пустых рабочих потоков между экземплярами несвязанных приложений. Это вовсе не "предприятие".

Если вы действительно собираетесь обмениваться потоками, java.lang.Thread (как минимум, в среде JavaEE) должны поддерживать такие методы, как setContextState(int key) и forgetContextState(int key) (mirroring setClasLoaderContext()), которые позволяют изолировать контейнер специфичное для приложения состояние ThreadLocal, поскольку оно передает поток между различными приложениями.

В ожидании таких изменений в пространстве имен java.lang разумно, чтобы разработчики приложений принимали правило "один поток-пул, один пример связанных приложений" и разработчикам приложений предполагалось, что "этот поток принадлежит мне до тех пор, пока ThreadDeath мы делаем часть '.