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

时间:2011-10-28 04:02:05

标签: java tomcat nio thread-local

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

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

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

3 个答案:

答案 0 :(得分:7)

只有熟悉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连接器应该没有区别 - 容器将调用servlet上的service方法,servlet将在单个线程中执行,然后最后完成所有操作。只是容器能够在处理连接时以更有效的方式处理IO。

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

HTH。

答案 1 :(得分:3)

要确认,您仍然可以检查一个处理请求的线程 来自tomcat邮件列表的here

答案 2 :(得分:1)

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

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

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

原始问题是指Spring,所以一个很好的例子是RequestContextHolder,它有一个ThreadLocal。假设NIO事件循环线程命名为“http-nio-8080-exec-1”,它将控制传递给AsyncResponse资源,然后通过Executor启动一个新线程(名为“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 ...因此将Map的引用从NIO事件循环Thread传递给AsyncResponse Thread是不安全的......你需要复制Map并传递它。有关如何执行此操作的示例,请参阅MDCAwareThreadPoolExectutor

基本上,您需要检查需要从NIO事件循环线程传递到AsyncResponse线程的每个ThreadLocal变量...并查看是否可以安全地传递对原始Object的引用,或者在将副本设置为工作线程的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());
});
相关问题