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

发布于 2024-12-12 05:38:52 字数 266 浏览 0 评论 0原文

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

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

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

This just came to mind when testing the Tomcat NIO connector during my load tests. I make use of ThreadLocal's additionally I use Spring, which I know in several places it also makes use of it.

Since the NIO connector does not have a thread per connection, I worry that it may result in very hard to find bugs if a ThreadLocal object was shared with another thread before it had been cleaned up. However, I assume that this is not an issue as it is not a documented warning that I could find, nor have I found any other posts warning about this. I assume that the NIO connector has no effect to the threads that serve the actual requests.

Before I go with this assumption, I was hoping to find some concrete proof.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(3

—━☆沉默づ 2024-12-19 05:38:52

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

首先,你需要明确你是指简单地使用 NIO 连接器还是也在谈论异步小服务程序。
每种情况的答案都会略有不同。

需要注意的主要事情是 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。

Only someone familiar with the Tomcat code will be able to give you a concrete answer, but I'll try a wooden one :)

Firstly, you need to be clear whether you mean simply using NIO connectors or whether you're also talking about Async servlets.
The answer will be slightly different in each case.

The main thing to be aware of is that Java doesn't have any sort of continuations, co-routines or thread-rescheduling. Which means that once you launch a piece of code running on a thread, only that piece of code will run on the thread until it completes.

So if you have: myObject.doSomething(); then for the time doSomething runs, it has exclusive access to that thread. The thread is not going to switch to some other piece of code - regardless of what sort of IO model you're using.

What might (will) happen is that different threads will be scheduled to run on different CPUs, but each thread will run one piece of code to completion.

So if doSomething is:

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

then there's nothing to worry about - doSomethingElse will run one a single thread and the threadlocal will be set to the right value for the whole execution.

So a simple NIO connector should make no difference - the container will call the service method on the servlet, the servlet will execute in a single thread, and then at the end it's all done. It's just that the container is able to process the IO in a more efficient way as it handles the connections.

If you're using async servlets then it's a little different - in that case your servlet might get called multiple times for a single request (because of the way the asynchronous model works), and those calls might be on different threads, so you can't store something in a thread-local between invocations of your servlet. But for a single call to your service method, it's still fine.

HTH.

天生の放荡 2024-12-19 05:38:52

为了确认这一点,您可以检查一下,这仍然是一个处理请求的线程
此处来自 tomcat 邮件列表

To confirm, that's still one thread which process a request as you can check
here from tomcat mailing list

雪落纷纷 2024-12-19 05:38:52

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

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

请注意,每个新请求也可以创建存储在 ThreadLocal 中的对象的新实例...在这种情况下,每个新请求都不会破坏先前请求期间存储在同一 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());
});

To add to the accepted answer from Tim and the follow up question from pacman, you do need to be careful when using the AsyncResponse or similar feature together with the NIO connector. I'm not sure what Tim means by, "your [async] servlet might get called multiple times for a single request" ... but if a "request" refers to a single "GET", "PUT", "POST", or "DELETE" then AFAIK that will result in a single call to the corresponding resource method in your servlet.

One issue you could run into with ThreadLocals and async resources is if the processing Thread in the async resource needs a copy of the ThreadLocal variable from the NIO event loop Thread. In other words, the NIO event loop Thread accepts a request then passes control to your async resource ... then that resource passes control to a child Thread ... then the NIO event loop Thread is free to handle another request ... so any ThreadLocal variables in the NIO event loop Thread might be stomped on by the subsequent request.

Note that it's also possible for each new request to make a new instance of the Object stored in the ThreadLocal ... in which case each new request will not stomp on the old instances that had been stored in the same ThreadLocal during previous requests ... but you need to be sure which case you are dealing with ... let's look at some examples.

The original question refers to Spring so a good example is the RequestContextHolder which has a ThreadLocal. Let's say the NIO event loop Thread is named, "http-nio-8080-exec-1" and it passes control to an AsyncResponse resource that then launches a new Thread (named "pool-2-thread-3") via an Executor. The new Thread has code that needs something from the RequestAttributes to get the answer to pass back via AsyncResponse.resume(). Since the code executing in Thread "pool-2-thread-3" needs to access the RequestAttributes from "http-nio-8080-exec-1" then you need to make sure of two things:

1) Your resource grabs a reference to the RequestAttributes from "http-nio-8080-exec-1" and passes it into "pool-2-thread-3"

2) When "http-nio-8080-exec-1" accepts a new request it will make a new copy of RequestAttributes and set that into it's ThreadLocal copy for RequestContextHolder for the new request (note, Spring code does work this way, so it's safe).

A contrary example is the log4j MDC ThreadLocal copy of the Map. In this case each new request reuses the same Map ... so it's not safe to pass the reference of the Map from the NIO event loop Thread to the AsyncResponse Thread ... you need to make a copy of the Map and pass that. See MDCAwareThreadPoolExectutor for an example of how to do that.

Basically, you'll need to check each ThreadLocal variable that you need to pass from the NIO event loop Thread into your AsyncResponse Thread ... and see if it is safe to just pass a reference to the original Object, or if you need to make a copy of the Object before setting the copy into the worker Thread's ThreadLocal variable.

BTW, Here's some code that combines the two examples above:

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

From your AsyncResponse resource simply make a call like this:

executor.execute(() -> {
    // veryLongOperation() needs to access the RequestAttributes and the MDC
    asyncResponse.resume(veryLongOperation());
});
~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文