Безопасно ли использовать ThreadLocal с Tomcat NIO Connector? - PullRequest
7 голосов
/ 28 октября 2011

Это пришло в голову при тестировании разъема Tomcat NIO во время моих нагрузочных тестов. Я использую ThreadLocal, кроме того, я использую Spring, который, как я знаю, в нескольких местах также использует его.

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

Прежде чем перейти к этому предположению, я надеялся найти какое-то конкретное доказательство.

Ответы [ 3 ]

7 голосов
/ 28 октября 2011

Только тот, кто знаком с кодом Tomcat, сможет дать вам конкретный ответ, но я попробую деревянный:)

Во-первых, вам необходимо уточнить, имеете ли вы в виду просто использование разъемов NIO или речь идет также об асинхронных сервлетах. Ответ будет немного отличаться в каждом случае.

Главное, о чем нужно знать, это то, что в Java нет никаких продолжений, сопрограмм или перепланирования потоков. Это означает, что после запуска фрагмента кода, выполняющегося в потоке, только этот фрагмент кода будет выполняться в потоке до его завершения.

Так что если у вас есть: myObject.doSomething();, то на время doSomething он имеет эксклюзивный доступ к этому потоку. Поток не собирается переключаться на какой-то другой фрагмент кода - независимо от того, какую модель ввода-вывода вы используете.

Что может (будет) случиться, так это то, что разные потоки будут запланированы для запуска на разных процессорах, но каждый поток выполнит один кусок кода до завершения.

Так что если doSomething это:

public static final ThreadLocal<MyClass> VALUE = new ThreadLocal<MyClass>();
public void doSomething() {
  VALUE.set(this);
  try {
    doSomethingElse();
  } finally {
    VALUE.set(null);
  }
}

тогда не о чем беспокоиться - doSomethingElse будет запускать один отдельный поток, а для локального потока будет установлено правильное значение для всего выполнения.

Таким образом, простой соединитель NIO не должен иметь никакого значения - контейнер вызовет метод service для сервлета, сервлет будет выполняться в одном потоке, а затем в конце все будет сделано. Просто контейнер способен обрабатывать ввод-вывод более эффективно, так как он обрабатывает соединения.

Если вы используете асинхронные сервлеты, то это немного отличается - в этом случае ваш сервлет может вызываться несколько раз за один запрос (из-за способа работы асинхронной модели), и эти вызовы могут быть в разных потоках, так что вы не можете хранить что-то в локальном потоке между вызовами вашего сервлета. Но для одного обращения к вашему методу обслуживания все еще хорошо.

НТН.

3 голосов
/ 30 июля 2013

Для подтверждения, это все еще один поток, который обрабатывает запрос, вы можете проверить здесь из списка рассылки tomcat

1 голос
/ 13 октября 2016

Чтобы добавить к принятому ответу Тима и последующий вопрос от pacman, вам нужно быть осторожным при использовании AsyncResponse или аналогичной функции вместе с разъемом NIO.Я не уверен, что Тим имеет в виду, «ваш [асинхронный] сервлет может быть вызван несколько раз за один запрос» ... но если «запрос» ссылается на один «GET», «PUT», «POST»или «УДАЛИТЬ», а затем AFAIK, что приведет к одному вызову соответствующего метода ресурса в вашем сервлете.

Одна проблема, с которой вы можете столкнуться с ThreadLocals и асинхронными ресурсами, - это если поток обработки в асинхронных ресурсах нуждаетсякопия переменной ThreadLocal из цикла обработки событий NIO Thread.Другими словами, цикл событий NIO Thread принимает запрос, затем передает управление вашему асинхронному ресурсу ... затем этот ресурс передает управление дочернему потоку ... затем цикл события NIO Thread может обработать другой запрос ... таклюбые переменные ThreadLocal в цикле событий NIO. Поток может быть растоптан последующим запросом.

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

Исходный вопрос относится к Spring, поэтому хорошим примером является RequestContextHolder, который имеет ThreadLocal.Допустим, цикл обработки событий NIO имеет имя «http-nio-8080-exec-1» и передает управление ресурсу AsyncResponse, который затем запускает новый поток (с именем «pool-2-thread-3») через исполнителя,Новый поток имеет код, которому нужно что-то из RequestAttributes, чтобы получить ответ для передачи обратно через AsyncResponse.resume ().Поскольку код, выполняющийся в потоке "pool-2-thread-3", должен получить доступ к атрибутам RequestAttributes из "http-nio-8080-exec-1", вам необходимо убедиться в двух вещах:

1)Ваш ресурс получает ссылку на атрибуты RequestAttributes из "http-nio-8080-exec-1" и передает ее в "pool-2-thread-3"

2) Когда "http-nio-8080-exec-1 "принимает новый запрос, создает новую копию RequestAttributes и устанавливает ее в свою ThreadLocal копию для RequestContextHolder для нового запроса (обратите внимание, код Spring работает таким образом, поэтому он безопасен).

Aпротивоположным примером является копия LogCj MDC ThreadLocal карты.В этом случае каждый новый запрос повторно использует одну и ту же карту ..., поэтому небезопасно передавать ссылку на карту из потока событий NIO в поток AsyncResponse ... вам нужно сделать копию карты и передать ее.См. MDCAwareThreadPoolExectutor для примера того, как это сделать.

По сути, вам необходимо проверить каждую переменную ThreadLocal, которую нужно передать из потока событий NIO Thread в ваш поток AsyncResponse.... и посмотреть, безопасно ли просто передать ссылку на исходный объект или вам нужно сделать копию объекта перед установкой копии в переменную ThreadLocal рабочего потока.

Кстати, вот некоторыекод, который объединяет два приведенных выше примера:

public class RequestContextAwareThreadPoolExecutor extends MDCAwareThreadPoolExecutor {
    /* ... constructors left out ... */

    @Override
    public void execute(Runnable runnable) {
        super.execute(wrap(runnable, RequestContextHolder.currentRequestAttributes()));
    }

    Runnable wrap(final Runnable runnable, final RequestAttributes requestAttributes) {
        return () -> {
            RequestContextHolder.setRequestAttributes(requestAttributes);
            try {
                runnable.run();
            } finally {
                RequestContextHolder.resetRequestAttributes();
            }
        };
    }
}

Из вашего ресурса AsyncResponse просто позвоните так:

executor.execute(() -> {
    // veryLongOperation() needs to access the RequestAttributes and the MDC
    asyncResponse.resume(veryLongOperation());
});
...