Repository: netty/netty
Issue: Multithreading reclaiming objects in the Recycler object pool issue problem

### Netty version
4.1.70.final

### Issue

The issue shown in the picture below，Thread 2 is recycling objects which belongs to stack1 created by thread 1。

If thread1 was GC'ed at this moment，i think thread2 does not need to add this object to the `weakOrderQueue node` which is in the `Stack1` any more。 because `stack1` will be GC'ed sooner or later。This object will never be used again even if it is recycled.

![netty recycler issue](https://user-images.githubusercontent.com/5794695/143539720-36080ab2-458e-4c52-b6cb-db0c2f6f602d.png)

But the `Recycler`'s related source code about recycling objects does not deal with the situation of `threadRef.get()  == null`,
and still continue to add objects to the weakOrderQueue node. I think this is a waste of time.  the sooner we deal with the situation of `threadRef.get()  == null` , the better。

```
private static final class Stack<T> {

        void push(DefaultHandle<?> item) {
            Thread currentThread = Thread.currentThread();
            if (threadRef.get() == currentThread) {
                // The current Thread is the thread that belongs to the Stack, we can try to push the object now.
                pushNow(item);
            } else {
                // The current Thread is not the one that belongs to the Stack
                // (or the Thread that belonged to the Stack was collected already), we need to signal that the push
                // happens later.
                pushLater(item, currentThread);
            }
        }

}
```


Issue comments:
@chrisvest I think the following issue seems still exists in the newest 4.1.72.Final version.

"When thread2 is recycling objects which created by thread1, if thread1 was finished/terminated at this moment, then thread2 will still put the objects back to thread1's queue, which is not necessary."

Corrects me if I wrongly understood it.
@laosijikaichele Right, yeah… I'm not sure it makes sense to salvage those objects. We could be in a system shutdown phase; there might not be other threads to offload the objects to; we'd have to centrally track threads joining and leaving the pool.
@chrisvest  What about we store the thread to `WeakReference` in `io.netty.util.Recycler.LocalPool`, and do the `null` check while recycling, like the following:
```
private static final class LocalPool<T> {
    ...
    private final WeakReference<Thread> threadRef = new WeakReference<>(Thread.currentThread());
    ...
}

private static final class DefaultHandle<T> implements Handle<T> {
    @Override
    public void recycle(Object object) {
        ...
        if (localPool.threadRef.get() == null) {
            return; // Do not need to recycle;
        }
        ...
    }
}
```
WDYT?
@laosijikaichele Not a fan of adding back `WeakReferences` when we just got rid of them. They have some overhead that can impact GC pause times. But we can mark the local pool in `FastThreadLocal.onRemoval` somehow, and avoid adding handles to the queue based on that.
@chrisvest It sounds good. :) 

Pull request review comments:
Remove ?
Removed.

Patch that resolved the issue:
diff --git a/common/src/main/java/io/netty/util/Recycler.java b/common/src/main/java/io/netty/util/Recycler.java
index b80c749df16..f518aaa09b4 100644
--- a/common/src/main/java/io/netty/util/Recycler.java
+++ b/common/src/main/java/io/netty/util/Recycler.java
@@ -102,7 +102,9 @@ protected LocalPool<T> initialValue() {
         @Override
         protected void onRemoval(LocalPool<T> value) throws Exception {
             super.onRemoval(value);
-            value.pooledHandles.clear();
+            MessagePassingQueue<DefaultHandle<T>> handles = value.pooledHandles;
+            value.pooledHandles = null;
+            handles.clear();
         }
     };
 
@@ -254,7 +256,7 @@ void toAvailable() {
 
     private static final class LocalPool<T> {
         private final int ratioInterval;
-        private final MessagePassingQueue<DefaultHandle<T>> pooledHandles;
+        private volatile MessagePassingQueue<DefaultHandle<T>> pooledHandles;
         private int ratioCounter;
 
         @SuppressWarnings("unchecked")
@@ -269,16 +271,23 @@ private static final class LocalPool<T> {
         }
 
         DefaultHandle<T> claim() {
+            MessagePassingQueue<DefaultHandle<T>> handles = pooledHandles;
+            if (handles == null) {
+                return null;
+            }
             DefaultHandle<T> handle;
             do {
-                handle = pooledHandles.relaxedPoll();
+                handle = handles.relaxedPoll();
             } while (handle != null && !handle.availableToClaim());
             return handle;
         }
 
         void release(DefaultHandle<T> handle) {
+            MessagePassingQueue<DefaultHandle<T>> handles = pooledHandles;
             handle.toAvailable();
-            pooledHandles.relaxedOffer(handle);
+            if (handles != null) {
+                handles.relaxedOffer(handle);
+            }
         }
 
         DefaultHandle<T> newHandle() {


Test patch: