Использование ThreadLocal для обхода Servlet threadunsafety?

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

public class ExampleServlet extends HttpServlet {

    private Object thisIsNOTThreadSafe;

    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        Object thisIsThreadSafe;

        thisIsNOTThreadSafe = request.getParameter("foo"); // BAD!! Shared among all requests!
        thisIsThreadSafe = request.getParameter("foo"); // OK, this is thread safe.
    } 
}

На самом деле, люди, которые кодировали мой сервлет, похоже, тоже знали об этом, но решили обойти это, сделав что-то вроде этого:

public class ExampleServlet extends HttpServlet {

    private ThreadLocal<MyObject> thisIsMaybeThreadSafe = new ThreadLocal<MyObject>();;

    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        // we want to avoid having to use this parameter in every method
        thisIsMaybeThreadSafe.set((MyObject)getObjectInSesssion("foo"));
        doStuff(request, response);
    } 
}

И код также содержит такие вещи, как

synchronized(request.getAttribute("foo")){
   doStuff(request, response);
}

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

11:07:17,525 ERROR [com.mycompany.myproject.web.business.servlet.map.tree.MapServlet] Error processing AjaxTreeAccessRequest
java.lang.NullPointerException
    at org.apache.catalina.connector.Request.notifyAttributeAssigned(Request.java:1493)
    at org.apache.catalina.connector.Request.setAttribute(Request.java:1484)
    at org.apache.catalina.connector.RequestFacade.setAttribute(RequestFacade.java:539)
    at javax.servlet.ServletRequestWrapper.setAttribute(ServletRequestWrapper.java:244)
    at org.apache.myfaces.context.servlet.RequestMap.setAttribute(RequestMap.java:51)
    at org.apache.myfaces.util.AbstractAttributeMap.put(AbstractAttributeMap.java:108)
    at org.apache.myfaces.el.VariableResolverImpl.resolveVariable(VariableResolverImpl.java:304)
    at org.springframework.web.jsf.DelegatingVariableResolver.resolveOriginal(DelegatingVariableResolver.java:120)
    at org.springframework.web.jsf.DelegatingVariableResolver.resolveVariable(DelegatingVariableResolver.java:105)
    at com.mycompany.myproject.web.common.servlet.AbstractFacesServlet.getManagedBean(AbstractFacesServlet.java:67)
    at com.mycompany.myproject.web.business.servlet.map.tree.MapServlet.getSessionTreeBean(MapServlet.java:184)
    at com.mycompany.myproject.web.business.servlet.map.tree.AjaxTreeAccess.initRequest(AjaxTreeAccess.java:355)
    at com.mycompany.myproject.web.business.servlet.map.tree.AjaxTreeAccess.processRequest(AjaxTreeAccess.java:134)
    at com.mycompany.myproject.web.common.servlet.AbstractFacesServlet.handleRequest(AbstractFacesServlet.java:81)
    at org.springframework.web.context.support.HttpRequestHandlerServlet.service(HttpRequestHandlerServlet.java:63)
    at javax.servlet.http.HttpServlet.service(HttpServlet.java:725)
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:291)
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:206)
    at org.springframework.orm.hibernate3.support.OpenSessionInViewFilter.doFilterInternal(OpenSessionInViewFilter.java:198)
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:76)
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:239)
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:206)
    at com.mycompany.myproject.commun.presentation.jsf.OpenSynchronizedSessionInViewFilter.doFilterInternal(OpenSynchronizedSessionInViewFilter.java:58)
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:76)
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:239)
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:206)
    at org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:52)
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:239)
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:206)
    at org.acegisecurity.util.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:265)
    at com.mycompany.myproject.web.common.filter.SwitchUserProcessingFilter.doFilter(SwitchUserProcessingFilter.java:66)
    at org.acegisecurity.util.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:275)
    at org.acegisecurity.ui.ExceptionTranslationFilter.doFilter(ExceptionTranslationFilter.java:110)
    at org.acegisecurity.util.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:275)
    at org.acegisecurity.providers.anonymous.AnonymousProcessingFilter.doFilter(AnonymousProcessingFilter.java:125)
    at org.acegisecurity.util.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:275)
    at org.acegisecurity.wrapper.SecurityContextHolderAwareRequestFilter.doFilter(SecurityContextHolderAwareRequestFilter.java:81)
    at org.acegisecurity.util.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:275)
    at org.acegisecurity.ui.AbstractProcessingFilter.doFilter(AbstractProcessingFilter.java:229)
    at org.acegisecurity.util.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:275)
    at org.acegisecurity.ui.logout.LogoutFilter.doFilter(LogoutFilter.java:106)
    at org.acegisecurity.util.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:275)
    at org.acegisecurity.context.HttpSessionContextIntegrationFilter.doFilter(HttpSessionContextIntegrationFilter.java:286)
    at org.acegisecurity.util.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:275)
    at org.acegisecurity.util.FilterChainProxy.doFilter(FilterChainProxy.java:149)
    at org.acegisecurity.util.FilterToBeanProxy.doFilter(FilterToBeanProxy.java:98)
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:239)
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:206)
    at com.mycompany.myproject.web.business.filter.UserBindingFilter.doFilter(UtilisateurCourantBindingFilter.java:55)
    at org.springframework.web.filter.DelegatingFilterProxy.invokeDelegate(DelegatingFilterProxy.java:236)
    at org.springframework.web.filter.DelegatingFilterProxy.doFilter(DelegatingFilterProxy.java:167)
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:239)
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:206)
    at com.mycompany.myproject.web.common.filter.SessionTimeoutFilter.doFilter(SessionTimeoutFilter.java:67)
    at org.springframework.web.filter.DelegatingFilterProxy.invokeDelegate(DelegatingFilterProxy.java:236)
    at org.springframework.web.filter.DelegatingFilterProxy.doFilter(DelegatingFilterProxy.java:167)
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:239)
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:206)
    at com.mycompany.myproject.web.common.filter.SessionLoginFilter.doFilter(SessionLoginFilter.java:56)
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:239)
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:206)
    at com.mycompany.profiling.prof.filter.ProfContextFilter.doFilter(ProfContextFilter.java:26)
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:239)
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:206)
    at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:219)
    at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:106)
    at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:503)
    at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:136)
    at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:79)
    at org.apache.catalina.valves.AbstractAccessLogValve.invoke(AbstractAccessLogValve.java:610)
    at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:88)
    at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:526)
    at org.apache.coyote.http11.AbstractHttp11Processor.process(AbstractHttp11Processor.java:1078)
    at org.apache.coyote.AbstractProtocol$AbstractConnectionHandler.process(AbstractProtocol.java:655)
    at org.apache.coyote.http11.Http11NioProtocol$Http11ConnectionHandler.process(Http11NioProtocol.java:222)
    at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1566)
    at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.run(NioEndpoint.java:1523)
    at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1145)
    at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:615)
    at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)
    at java.lang.Thread.run(Thread.java:744)

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

К вашему сведению, все приложение находится в устаревшем коде Spring-JSF, а некоторые переменные ThreadLocal на самом деле являются bean-компонентами, связанными с сеансом JSF. Что мне нужно сделать, чтобы проверить глобальную потокобезопасность этого приложения?


person Aldian    schedule 26.03.2015    source источник
comment
Общее эмпирическое правило, которое хорошо служило мне, когда я работал с сервлетами: никогда, ни при каких обстоятельствах, нет, серьезно никогда не добавляйте в сервлет какое-либо состояние. Я знаю, что это мало поможет вам, так как ущерб уже нанесен, но это то, к чему вы должны стремиться, расчищая этот беспорядок. И удачи.   -  person biziclop    schedule 27.03.2015


Ответы (2)


  1. synchronized(request.getAttribute("foo")) плохо, потому что в запросе не может быть foo и вы получите NPE. Лучше использовать какой-нибудь выделенный объект блокировки.
  2. Что касается использования ThreadLocal - это нормально, если не злоупотреблять. Для кода того размера, который вы разместили, это нормально, но я думаю, что реальная проблема заключается в вашей реальной кодовой базе, и здесь практически невозможно дать короткий полезный совет, кроме того, что вам нужно написать много модульных тестов (как можно более простых) для свою реальную логику (а не ту, которая связана с Servlet API и параллелизмом), а затем постепенно рефакторинг кодовой базы до более разумного состояния.

Надеюсь это поможет :)

person Victor Sorokin    schedule 26.03.2015
comment
Моей отправной точкой для подозрения на проблему параллелизма был вопрос NullPointerException при установке атрибута?, потому что я точно есть такая же проблема. Должно быть что-то не так с тем, как используются ThreadLocal, но я продолжу искать. Спасибо. - person Aldian; 27.03.2015
comment
@Aldian Тогда вам следует добавить трассировку стека NPE в свой пост. ThreadLocal отлично работает при правильном использовании. - person Victor Sorokin; 27.03.2015
comment
Хм, NPE встречается где-то в коде библиотеки, а не в вашем коде. Вам следует попробовать отладить сайт NPE, проверить, что именно установлено в null и откуда оно берется (возможно, из вашего кода). - person Victor Sorokin; 27.03.2015
comment
Если вы посмотрите на код затронутой библиотеки, вы увидите, что строка 1493 — это Object listeners[] = context.getApplicationEventListeners();. NPE может произойти только в том случае, если контекст имеет значение null, что означает наличие проблемы с многопоточностью. Независимо от параметра, который я пытаюсь установить NPE, все равно произойдет - person Aldian; 27.03.2015

Этот ThreadLocal для обработки сеанса кажется мне опасным решением.

ThreadLocal создаст MyObject для каждого потока, вызывающего этот класс. Не каждый клиент.

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

Я могу ошибаться в том, как работает контейнер сервлета (и скажите мне, если я ошибаюсь), но на вашем месте я бы создал множество тестов параллелизма, чтобы убедиться, что у каждого клиента есть отдельный MyObject для его сессия.

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

Итак, я думаю, что в основном проблема заключается в том, что в этом примере кода входящее соединение перепутано с потоком, который вызывает doGet. Как вы думаете?

person gaRos    schedule 27.03.2015
comment
Вы должны взглянуть на вопрос, который я связал, который объясняет, как работают сервлеты. Например, сервлеты НЕ являются потокобезопасными. - person Aldian; 27.03.2015
comment
@Aldian: Вы правы насчет безопасности потоков сервлетов, я уберу это из своего ответа! - person gaRos; 27.03.2015