ThreadLocal是否可以安全地与Tomcat NIO Connector一起使用

在负载测试期间测试Tomcat NIO连接器时,我们想到了这一点。 我使用ThreadLocal另外我使用Spring,我知道在几个地方它也使用它。

由于NIO连接器每个连接没有一个线程,我担心如果ThreadLocal对象在清理之前与另一个线程共享,则可能导致很难找到错误。 但是,我认为这不是一个问题,因为它不是我能找到的文件警告,也没有发现任何其他post警告这一点。 我假设NIO连接器对服务于实际请求的线程没有影响。

在我采用这个假设之前,我希望找到一些具体的证据。

只有熟悉Tomcat代码的人才能给你一个具体的答案,但我会尝试一个木制的:)

首先,您需要明确是否只是使用NIO连接器,或者您是否也在谈论Async servlet。 答案在每种情况下都会略有不同。

需要注意的主要事情是Java没有任何类型的延续,协同例程或线程重新安排。 这意味着一旦你启动一个在线程上运行的代码, 只有 那段代码将在线程上运行直到它完成。

所以如果你有: myObject.doSomething(); 然后在doSomething运行的时候,它拥有对该线程的独占访问权限。 该线程不会切换到其他代码 – 无论您使用的是什么类型的IO模型。

可能(将)会发生的是,不同的线程将被安排在不同的CPU上运行,但每个线程将运行一段代码来完成。

所以如果doSomething是:

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

然后就没什么可担心的了 – doSomethingElse将运行一个线程,并且threadlocal将被设置为整个执行的正确值。

因此,简单的NIO连接器应该没有区别 – 容器将调用servlet上的service方法,servlet将在单个线程中执行,然后最后完成所有操作。 只是容器能够在处理连接时以更有效的方式处理IO。

如果您正在使用异步servlet,那么它会有所不同 – 在这种情况下,您的servlet可能会因单个请求被多次调用(因为异步模型的工作方式),并且这些调用可能位于不同的线程上,因此您可以在servlet的调用之间存储一些线程本地的东西。 但是对于一次调用您的服务方法,它仍然没问题。

HTH。

要确认,这仍然是处理请求的一个线程,您可以从tomcat邮件列表中查看

要添加Tim提供的接受答案以及pacman的后续问题,在将AsyncResponse或类似function与NIO连接器一起使用时,需要注意。 我不确定Tim意味着什么,“你的[异步] servlet可能会被多次调用一次请求”……但是如果“请求”指的是单个“GET”,“PUT”,“POST”或“DELETE”然后AFAIK将导致对servlet中相应资源方法的单个调用。

使用ThreadLocals和异步资源可能遇到的一个问题是,异步资源中的处理线程是否需要来自NIO事件循环线程的ThreadLocal变量的副本。 换句话说,NIO事件循环线程接受请求然后将控制传递给您的异步资源…然后该资源将控制传递给子线程…然后NIO事件循环线程可以自由处理另一个请求…所以NIO事件循环线程中的任何ThreadLocal变量都可能被后续请求踩踏。

请注意,每个新请求也可以创建一个存储在ThreadLocal中的Object的新实例…在这种情况下,每个新请求都不会踩踏在先前请求期间存储在同一ThreadLocal中的旧实例。但是你需要确定你正在处理哪种情况……让我们看看一些例子。

原始问题是指Spring,所以一个很好的例子是RequestContextHolder,它有一个ThreadLocal。 假设NIO事件循环线程命名为“http-nio-8080-exec-1”,它将控制传递给AsyncResponse资源,然后通过执行程序启动一个新线程(名为“pool-2-thread-3”) 。 新的Thread有一些代码需要RequestAttributes提供的东西,以获得通过AsyncResponse.resume()传回的答案。 由于在Thread“pool-2-thread-3”中执行的代码需要从“http-nio-8080-exec-1”访问RequestAttributes,因此您需要确保两件事:

1)您的资源从“http-nio-8080-exec-1”获取对RequestAttributes的引用,并将其传递给“pool-2-thread-3”

2)当“http-nio-8080-exec-1”接受新请求时,它将生成RequestAttributes的新副本,并将其设置为RequestContextHolder的新请求的ThreadLocal副本(注意,Spring代码确实以这种方式工作,所以它是安全的)。

一个相反的例子是Map的log4j MDC ThreadLocal副本。 在这种情况下,每个新请求都重用相同的Map …因此将NIO事件循环线程中的Map引用传递给AsyncResponse线程是不安全的……您需要复制Map并传递它。 有关如何执行此操作的示例,请参阅MDCAwareThreadPoolExectutor 。

基本上,您需要检查需要从NIO事件循环线程传递到AsyncResponse线程的每个ThreadLocal变量…并查看是否可以安全地传递对原始对象的引用,或者如果您需要在将副本设置到工作线程的ThreadLocal变量之前制作Object的副本。

顺便说一下,这里结合了上面两个例子:

 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()); });