Is ThreadLocal safe to use with Tomcat NIO Connect

2020-06-17 05:13发布

问题:

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.

回答1:

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.



回答2:

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



回答3:

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