When analyzing the risks of passing the Request object into asynchronous threads, a critical question emerges: is the reused Request bound to a specific thread?
Logging the hash codes of Request objects across multiple HTTP calls handled by different threads from the pool reveals that the same Request instance can be accessed by distinct threads. This confirms that Request instances are not strictly coupled to execution threads. A decoupled design implies the existence of an object pool where threads acquire available Request instances rather than maintaining a one-to-one relationship.
Tracing the Object Creation
To understand the reuse mechanism, the first debugging step involves identifying where the RequestFacade is instantiated. Setting a breakpoint in the RequestFacade constructor demonstrates that subsequent requests often bypass this constructor, confirming instance reuse.
Inside the connector package, the creation logic checks if a facade instance already exists. If it does, it is reused and assigned to the applicationRequest field. The applicationRequest is cleared to null during the recycle() phase at the end of the request lifecycle. However, the facade is only cleared if the getDiscardFacades() method returns true. This behavior is governed by the RECYCLE_FACADES system property, which defaults to false for performance reasons, allowing the facade to persist between requests.
The Processor Pool
Tracing the call stack backward leads to the Http11Processor#service method, where the Request object is initially passed as an argument. The Request is instantiated within the AbstractProcessor constructor, which is invoked by createProcessor.
The key to the reuse mechanism lies in the conditional logic preceding object creation:
Processor activeProcessor = recycledProcessors.pop();
if (activeProcessor == null) {
activeProcessor = createProcessor();
}If recycledProcessors.pop() returns an available processor, a new one is not created. The RecycledProcessors class extends SynchronizedStack, acting as a thread-safe object pool designed to reduce garbage collection overhead.
Conversely, when a request completes, the processor is pushed back into this stack via the push method during the release phase. This creates a complete lifecycle: acquire from the pool (or create), process the request, and return to the pool.
Verifying the Processor-Request Relationship
To confirm the correlation between the Processor, the internal org.apache.coyote.Request, and the RequestFacade, logging statements can be injected by overriding the relevant Tomcat classes in the project's source tree.
It is crucial to distinguish between org.apache.coyote.Request and org.apache.catalina.connector.Request. The latter wraps the former via the setCoyoteRequest() method within CoyoteAdapter#service. Logging their hash codes confirms a 1:1:1 relationship between the Processor, the Coyote Request, and the RequestFacade.
Because the RequestFacade is tied to the Processor, reuse happens when the same Processor handles multiple sequential requests. Concurrent requests will force the pool to pop different Processor instances, resulting in distinct RequestFacade objects. The specific Processor assigned depends entirely on the LIFO (Last-In-First-Out) behavior of the recycledProcessors stack.
Cache Size and Performance Implications
The default capacity of the recycledProcessors pool is 200, aligning with the default maximum number of Tomcat worker threads. Maintaining parity between thread count and processor cache size is logically sound.
If the cache size is artificially restricted, for example, by setting server.tomcat.processor-cache=1 under high concurrency, the stack will reject push operations once full. Rejected processors trigger the unregister() method. Since unregister() and register() (called during processor creation) share a synchronized lock, frequent cache misses induce heavy lock contention, severely degrading performance.
Mitigating Lifecycle Pollution
Accessing a Request outside its lifecycle (e.g., in an async thread) causes data pollution when the object is reused. Beyond the standard asynchronous API (startAsync), two system-level configurations can address this.
First, enabling -Dorg.apache.catalina.connector.RECYCLE_FACADES=true forces the server to destroy the RequestFacade after each request. If an async thread attempts to access the recycled facade, the server immediately throws an exception, effectively enforcing lifecycle safety.
Second, setting server.tomcat.processor-cache=0 disables object pooling entirely. Every request receives a brand-new Processor and Request instance, completely eliminating reuse at the cost of increased garbage collection overhead.