这只是在我的负载测试期间测试 Tomcat NIO 连接器时想到的。我使用 ThreadLocal 的另外我使用 Spring,我知道它在几个地方也使用了它。
由于 NIO 连接器没有每个连接的线程,我担心如果 ThreadLocal 对象在清理之前与另一个线程共享,可能会导致很难找到错误。但是,我认为这不是问题,因为它不是我能找到的书面警告,也没有发现任何其他关于此的警告。我假设 NIO 连接器对服务于实际请求的线程没有影响。
在我提出这个假设之前,我希望能找到一些具体的证据。
这只是在我的负载测试期间测试 Tomcat NIO 连接器时想到的。我使用 ThreadLocal 的另外我使用 Spring,我知道它在几个地方也使用了它。
由于 NIO 连接器没有每个连接的线程,我担心如果 ThreadLocal 对象在清理之前与另一个线程共享,可能会导致很难找到错误。但是,我认为这不是问题,因为它不是我能找到的书面警告,也没有发现任何其他关于此的警告。我假设 NIO 连接器对服务于实际请求的线程没有影响。
在我提出这个假设之前,我希望能找到一些具体的证据。
只有熟悉 Tomcat 代码的人才能给你一个具体的答案,但我会尝试一个木制的 :)
首先,您需要清楚您的意思是简单地使用 NIO 连接器,还是您还谈论 Async servlet。在每种情况下,答案都会略有不同。
需要注意的主要事情是 Java 没有任何类型的延续、协同例程或线程重新调度。这意味着一旦你启动了在线程上运行的一段代码,只有 那段代码才会在线程上运行,直到它完成。
因此,如果您有:myObject.doSomething();
那么对于时间doSomething
运行,它具有对该线程的独占访问权限。该线程不会切换到其他一些代码 - 无论您使用哪种 IO 模型。
可能(将会)发生的是,不同的线程将被安排在不同的 CPU 上运行,但每个线程将运行一段代码来完成。
所以如果doSomething
是:
public static final ThreadLocal<MyClass> VALUE = new ThreadLocal<MyClass>();
public void doSomething() {
VALUE.set(this);
try {
doSomethingElse();
} finally {
VALUE.set(null);
}
}
那么就没有什么可担心的了 -doSomethingElse
将在单个线程中运行一个线程,并且 threadlocal 将被设置为整个执行的正确值。
所以一个简单的 NIO 连接器应该没有什么区别——容器将调用service
servlet 上的方法,servlet 将在单个线程中执行,最后一切都完成了。只是容器在处理连接时能够以更有效的方式处理 IO。
如果您使用的是异步 servlet,那么情况会有些不同 - 在这种情况下,您的 servlet 可能会为单个请求多次调用(因为异步模型的工作方式),并且这些调用可能在不同的线程上,所以您可以'不要在你的 servlet 调用之间将某些东西存储在线程本地中。但是对于您的服务方法的一次调用,它仍然可以。
HTH。
确认一下,这仍然是一个处理请求的线程,您可以 从 tomcat 邮件列表中查看此处
要添加来自 Tim 的已接受答案和来自 pacman 的后续问题,在将 AsyncResponse 或类似功能与 NIO 连接器一起使用时需要小心。我不确定 Tim 的意思是什么,“您的 [async] servlet 可能会为单个请求多次调用”......但是如果“请求”指的是单个“GET”、“PUT”、“POST” ,或“DELETE”,然后是 AFAIK,这将导致对 servlet 中相应资源方法的一次调用。
使用 ThreadLocals 和异步资源可能会遇到的一个问题是,异步资源中的处理线程是否需要 NIO 事件循环线程中的 ThreadLocal 变量的副本。换句话说,NIO 事件循环线程接受一个请求,然后将控制权传递给您的异步资源……然后该资源将控制权传递给子线程……然后 NIO 事件循环线程可以自由处理另一个请求……所以NIO 事件循环线程中的任何 ThreadLocal 变量都可能被后续请求踩踏。
请注意,每个新请求也可以创建存储在 ThreadLocal 中的 Object 的新实例......在这种情况下,每个新请求都不会踩到在先前请求期间存储在同一 ThreadLocal 中的旧实例...... .但是您需要确定您正在处理哪种情况...让我们看一些示例。
最初的问题是指 Spring,所以一个很好的例子是具有 ThreadLocal 的 RequestContextHolder。假设 NIO 事件循环线程被命名为“http-nio-8080-exec-1”,它将控制权传递给 AsyncResponse 资源,然后通过 Executor 启动一个新线程(名为“pool-2-thread-3”) . 新线程的代码需要 RequestAttributes 中的某些内容来获得通过 AsyncResponse.resume() 传回的答案。由于在线程“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 ...因此将 Map 的引用从 NIO 事件循环线程传递到 AsyncResponse 线程是不安全的 ...您需要制作 Map 的副本并传递它。有关如何执行此操作的示例,请参阅MDCAwareThreadPoolExectutor 。
基本上,您需要检查需要从 NIO 事件循环线程传递到 AsyncResponse 线程的每个 ThreadLocal 变量......并查看是否只传递对原始对象的引用是否安全,或者是否需要在将副本设置到工作线程的 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());
});