写点什么

netty 系列之: 可能有人听过 ThreadLocal, 但一定没人听过 ThreadLocal 对象池

作者:程序那些事
  • 2022 年 4 月 28 日
  • 本文字数:4022 字

    阅读完需:约 13 分钟

netty系列之:可能有人听过ThreadLocal,但一定没人听过ThreadLocal对象池

简介

JDK 中的 Thread 大家肯定用过,只要是用过异步编程的同学肯定都熟悉。为了保存 Thread 中特有的变量,JDK 引入了 ThreadLocal 类来专门对 Thread 的本地变量进行管理。

ThreadLocal

很多新人可能不明白 ThreadLocal 到底是什么,它和 Thread 到底有什么关系。


其实很简单,ThreadLocal 本质上是一个 key,它的 value 就是 Thread 中一个 map 中存储的值。


每个 Thread 中都有一个 Map, 这个 Map 的类型是 ThreadLocal.ThreadLocalMap。我们先不具体讨论这个 ThreadLocalMap 到底是怎么实现的。现在就简单将其看做是一个 map 即可。


接下来,我们看下一个 ThreadLocal 的工作流程。


首先来看一下 ThreadLocal 的使用例子:


   public class ThreadId {       // 一个线程ID的自增器       private static final AtomicInteger nextId = new AtomicInteger(0);         // 为每个Thread分配一个线程       private static final ThreadLocal<Integer> threadId =           new ThreadLocal<Integer>() {               @Override protected Integer initialValue() {                   return nextId.getAndIncrement();           }       };         // 返回当前线程的ID       public static int get() {           return threadId.get();       }   }
复制代码


上面的类是做什么用的呢?


当你在不同的线程环境中调用 ThreadId 的 get 方法时候,会返回不同的 int 值。所以可以看做是 ThreadId 为每个线程生成了一个线程 ID。


我们来看下它是怎么工作的。


首先我们调用了 ThreadLocal<Integer>的 get 方法。ThreadLocal 中的 get 方法定义如下:


    public T get() {        Thread t = Thread.currentThread();        ThreadLocalMap map = getMap(t);        if (map != null) {            ThreadLocalMap.Entry e = map.getEntry(this);            if (e != null) {                @SuppressWarnings("unchecked")                T result = (T)e.value;                return result;            }        }        return setInitialValue();    }
复制代码


get 方法中,我们第一步获取当前的线程 Thread,然后 getMap 返回当前 Thread 中的 ThreadLocalMap 对象。


如果 Map 不为空,则取出以当前 ThreadLocal 为 key 对应的值。


如果 Map 为空,则调用初始化方法:


    private T setInitialValue() {        T value = initialValue();        Thread t = Thread.currentThread();        ThreadLocalMap map = getMap(t);        if (map != null)            map.set(this, value);        else            createMap(t, value);        return value;    }
复制代码


初始化方法首先判断当前 Thread 中的 ThreadLocalMap 是否为空,如果不为空则设置初始化的值。


如果为空则创建新的 Map:


    void createMap(Thread t, T firstValue) {        t.threadLocals = new ThreadLocalMap(this, firstValue);    }
复制代码


大家可以看到整个 ThreadLocalMap 中的 Key 就是 ThreadLocal 本身,而 Value 就是 ThreadLocal 中定义的泛型的值。


现在我们来总结一下 ThreadLocal 到底是做什么的。


每个 Thread 中都有一个 ThreadLocal.ThreadLocalMap 的 Map 对象,我们希望向这个 Map 中存放一些特定的值,通过一个特定的对象来访问到存放在 Thread 中的这个值,这样的对象就是 ThreadLocal。


通过 ThreadLocal 的 get 方法,就可以返回绑定到不同 Thread 对象中的值。

ThreadLocalMap

上面我们简单的将 ThreadLocalMap 看做是一个 map。事实上 ThreadLocalMap 是一个对象,它里面存放的每个值都是一个 Entry.


这个 Entry 不同于 Map 中的 Entry,它是一个 static 的内部类:


        static class Entry extends WeakReference<ThreadLocal<?>> {            /** The value associated with this ThreadLocal. */            Object value;
Entry(ThreadLocal<?> k, Object v) { super(k); value = v; } }
复制代码


注意,这里的 Entry 继承自 WeakReference,表示这个 Entry 的 key 是弱引用对象,如果 key 没有强引用的情况下,会在 gc 中被回收。从而保证了 Map 中数据的有效性。


ThreadLocalMap 中的值都存放在 Entry 数组中:


private Entry[] table;
复制代码


我们看一下怎么从 ThreadLocalMap 中 get 一个值的:


        private Entry getEntry(ThreadLocal<?> key) {            int i = key.threadLocalHashCode & (table.length - 1);            Entry e = table[i];            if (e != null && e.get() == key)                return e;            else                return getEntryAfterMiss(key, i, e);        }
复制代码


key.threadLocalHashCode 可以简单的看做是 ThreadLocal 代表的 key 的值。


而 key.threadLocalHashCode & (table.length - 1) 则使用来计算当前 key 在 table 中的 index。


这里使用的是位运算,用来提升计算速度。实际上这个计算等同于:


key.threadLocalHashCode % table.length
复制代码


是一个取模运算。


如果按照取模运算的 index 去查找,找到就直接返回。


如果没找到则会遍历调用 nextIndex 方法,修改 index 的值,只到查找完毕为止:


        private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {            Entry[] tab = table;            int len = tab.length;
while (e != null) { ThreadLocal<?> k = e.get(); if (k == key) return e; if (k == null) expungeStaleEntry(i); else i = nextIndex(i, len); e = tab[i]; } return null; }
复制代码

Recycler

ThreadLocal 本质上是将 ThreadLocal 这个对象和不同的 Thread 进行绑定,通过 ThreadLocal 的 get 方法可以获得存储在不同 Thread 中的值。


归根结底,ThreadLocal 和 Thread 是一对多的关系。因为 ThreadLocal 在 ThreadLocalMap 中是弱引用,所以当 ThreadLocal 被置为空之后,对应的 ThreadLocalMap 中的对象会在下一个垃圾回收过程中被回收,从而为 Thread 中的 ThreadLocalMap 节省一个空间。


那么当我们的 Thread 是一个长时间运行的 Thread 的时候,如果在这个 Thread 中分配了很多生命周期很短的对象,那么会生成很多待回收的垃圾对象,给垃圾回收器造成压力。


为了解决这个问题,netty 为我们提供了 Recycler 类,用来回收这些短生命周期的对象。接下来,我们来探究一下 Recycler 到底是怎么工作的。


在这之前,我们先看下怎么使用 Recycler。


public class MyObject {
private static final Recycler<MyObject> RECYCLER = new Recycler<MyObject>() { protected MyObject newObject(Recycler.Handle<MyObject> handle) { return new MyObject(handle); } }
public static MyObject newInstance(int a, String b) { MyObject obj = RECYCLER.get(); obj.myFieldA = a; obj.myFieldB = b; return obj; } private final Recycler.Handle<MyObject> handle; private int myFieldA; private String myFieldB;
private MyObject(Handle<MyObject> handle) { this.handle = handle; } public boolean recycle() { myFieldA = 0; myFieldB = null; return handle.recycle(this); }}
MyObject obj = MyObject.newInstance(42, "foo");...obj.recycle();
复制代码


本质上,Recycler 就像是一个工厂类,通过它的 get 方法来生成对应的类对象。当这个对象需要被回收的时候,调用 Recycler.Handle 中的 recycle 方法,即可将对象回收。


先看一下生成对象的 get 方法:


    public final T get() {        if (maxCapacityPerThread == 0) {            return newObject((Handle<T>) NOOP_HANDLE);        }        Stack<T> stack = threadLocal.get();        DefaultHandle<T> handle = stack.pop();        if (handle == null) {            handle = stack.newHandle();            handle.value = newObject(handle);        }        return (T) handle.value;    }
复制代码


上面代码的含义就是,先判断是否超过了单个线程允许的最大容量,如果是,则返回一个新的对象,绑定一个空的 handler,表示这个新创建的对象是不可以被回收的。


如果不是,则从 threadLocal 中拿到当前线程绑定的 Stack。然后从 Stack 中取出最上面的元素,如果 Stack 中没有对象,则创建新的对象,并绑定 handle。


最后返回 handle 绑定的对象。


再看一下 handle 的回收对象方法 recycle:


        public void recycle(Object object) {            if (object != value) {                throw new IllegalArgumentException("object does not belong to handle");            }
Stack<?> stack = this.stack; if (lastRecycledId != recycleId || stack == null) { throw new IllegalStateException("recycled already"); }
stack.push(this); }
复制代码


上面的代码先去判断和 handle 绑定的对象是不是要回收的对象。只有相等的时候才进行回收。


而回收的本质就是将对象 push 到 stack 中,供后续 get 的时候取出使用。


所以,Recycler 能够节约垃圾回收对象个数的原因是,它会将不再使用的对象存储到 Stack 中,并在下次 get 的时候返回,重复使用。这也就是我们在回收需要重置对象属性的原因:


  public boolean recycle() {    myFieldA = 0;    myFieldB = null;    return handle.recycle(this);  }
复制代码

总结

如果你在一个线程中会有多个同类型的短生命周期对象,那么不妨试试 Recycle 吧。


本文已收录于 http://www.flydean.com/47-netty-thread-…al-object-pool-2/

最通俗的解读,最深刻的干货,最简洁的教程,众多你不知道的小技巧等你来发现!

欢迎关注我的公众号:「程序那些事」,懂技术,更懂你!

发布于: 刚刚阅读数: 2
用户头像

关注公众号:程序那些事,更多精彩等着你! 2020.06.07 加入

最通俗的解读,最深刻的干货,最简洁的教程,众多你不知道的小技巧,尽在公众号:程序那些事!

评论

发布
暂无评论
netty系列之:可能有人听过ThreadLocal,但一定没人听过ThreadLocal对象池_Java_程序那些事_InfoQ写作社区