Netty 中 FastThreadLocal 分析

前一篇文章简单分析了Netty中PooledByteBuf 相关结构,本文将围绕 FastThreadLocal ,来分析netty中当前线程分配内存的设计。
本文基于 4.1.38.Final 版本

FastThreadLocal

开始 进行PoolThreadLocalCache 之前,先需要介绍 FastThreadLocal。这个很明显是在跟jdk的ThreadLocal 叫板,证明比他快。
先回一下jdk 的 ThreadLocal 相关原理吧:

ThreadLocal

估计大家都用过或者听过 Java 中 ThreadLocal,即本地线程变量。以下情况可能用到:

  1. Mybatis 的 ErrorContext 中有使用
  2. Dubbo 的 RpcContext 也有使用 ThreadLocal
  3. 在日常spring mvc 开发中,可以将认证信息放入ThreadLocal中,存储当前调用者信息。
ThreadLocal原理

在Thread 类 中,有一个 ThreadLocalMap 变量:

    ThreadLocal.ThreadLocalMap threadLocals = null;

从这里能猜出来,ThreadLocal果然是本地线程变量。

ThreadLocal 用法

正常情况下,使用 new ThreadLocal<T>() 即可使用ThreadLocal了
set方法

    public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }
  1. 从当前线程中,获取 threadLocals 变量
  2. value 放入map中。

接下来主要看看 ThreadLocalMap

ThreadLocalMap

存储在本地线程变量中的为 ThreadLocalMap,其内部是使用自定义Entry 来作为value维护这一个map。

    static class ThreadLocalMap {
        static class Entry extends WeakReference<ThreadLocal<?>> {
            /** The value associated with this ThreadLocal. */
            Object value;

            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }
        private Entry[] table;
  1. ThreadLocalMap 中map是以 Entry 作为key,且EntryThreadLocal 类型的key为弱引用 WeakReference 类型。
  2. value为普通类型

弱引用类型特性为,如果没有强引用关联,那么当每一次gc,无论内存是否够用,都会回收。
回过头看看 ThreadLocalMap 的set 方法:

        private void set(ThreadLocal<?> key, Object value) {
            Entry[] tab = table;
            int len = tab.length;
            int i = key.threadLocalHashCode & (len-1);

            for (Entry e = tab[i];
                 e != null;
                 e = tab[i = nextIndex(i, len)]) {
                ThreadLocal<?> k = e.get();
                if (k == key) {
                    e.value = value;
                    return;
                }
                if (k == null) {
                    replaceStaleEntry(key, value, i);
                    return;
                }
            }
            tab[i] = new Entry(key, value);
            int sz = ++size;
            if (!cleanSomeSlots(i, sz) && sz >= threshold)
                rehash();
        }
  1. 寻址方式为 key.threadLocalHashCode & (len-1);,即ThreadLocalthreadLocalHashCode 值:
    private final int threadLocalHashCode = nextHashCode();
    private static int nextHashCode() {
        return nextHashCode.getAndAdd(HASH_INCREMENT);
    }
    public final int getAndAdd(int delta) {
        return unsafe.getAndAddInt(this, valueOffset, delta);
    }

valueOffset 值则为 value 值内存中地址偏移量:

    private static final long valueOffset;

    static {
        try {
            valueOffset = unsafe.objectFieldOffset
                (AtomicInteger.class.getDeclaredField("value"));
        } catch (Exception ex) { throw new Error(ex); }
    }
  1. 遍历index 后面 所有map,如果 寻址定位到的 e 不为空,做了下面几件事:
  • 判断如果k等于当前ThreadLocal,则直接替换value 值,并退出
  • 如果k 等于null,说明该位置的弱引用ThreadLocal被回收掉了,那么执行 replaceStaleEntry 对其进行清理,
    replaceStaleEntry 里面其实做了挺多事,一旦发现有一个被弱引用回收的,肯定有可能不止一个:
    replaceStaleEntry 部分代码:
            for (int i = prevIndex(staleSlot, len);
                 (e = tab[i]) != null;
                 i = prevIndex(i, len))
                if (e.get() == null)
                    slotToExpunge = i;

所以他会往前找到第一个不为null 的 entry且他的key被gc回收过。然后从这个节点开始,进一步调用 expungeStaleEntry 进行清理。
最后将这个entry 放入map中,即设置tab的 staleSlot 位置为value值。

  • 如果没有需要释放或者没有找到已存在替换的,那么会 在检索到下标i的位置创建Entry,tab[i] = new Entry(key, value);
  • 判断是否需要扩容
ThreadLocal 缺陷
  1. 为什么在一个set方法,需要做这么多的事?
    因为ThreadLocal的 Entry 的key为WeakReference,但是value 为强引用。那么当ThreadLocal这个key被回收之后,value无法被回收,也无法被引用,造成了内存泄漏。
    所以ThreadLocal针对这个做了很多的事,先遍历,发现key为null,说明进行gc了,就遍历所有数组下标,将所有key为null且Entry 不为null的 Entry 清理掉。

Java 推荐我们将 ThreadLocal 设置为 static,并且用完就remove掉,来避免内存泄漏。

  1. ThreadLocal为了维护一个 ThreadLocalMap,hash寻址方法为 threadLocalHashCode & (INITIAL_CAPACITY - 1),但是hash冲突后方式为线性探测法,即往后面一直找,其实这样效率并不高(劣于HashMap的拉链法),极端情况下,要遍历整个 Entry 数组。
FastThreadLocal

基于上面对 ThreadLocal 分析,netty祭出了 FastThreadLocal 明显为了说明,我就是优于你。

FastThreadLocalThread 需要配合 FastThreadLocalThread 来使用,因为netty无法去更改jdk,所以netty创建一个子类来存储本地线程变量:

public class FastThreadLocalThread extends Thread {
    private InternalThreadLocalMap threadLocalMap;

结合ThreadLocal,总体存储原理到这里,也能猜出个大概。

FastThreadLocal 使用

** get() 方法**

    public final V get() {
        InternalThreadLocalMap threadLocalMap = InternalThreadLocalMap.get();
        Object v = threadLocalMap.indexedVariable(index);
        if (v != InternalThreadLocalMap.UNSET) {
            return (V) v;
        }
        return initialize(threadLocalMap);
    }
  1. InternalThreadLocalMapget 方法:
    public static InternalThreadLocalMap get() {
        Thread thread = Thread.currentThread();
        if (thread instanceof FastThreadLocalThread) {
            return fastGet((FastThreadLocalThread) thread);
        } else {
            return slowGet();
        }
    }

很容易理解出,如果是 FastThreadLocalThread 则从threadLocalMap 中取,否则从Thread 的变量中取。
2. Object v = threadLocalMap.indexedVariable(index); 寻址过程,就这么简单?
看index从哪里来?

    private final int index;
    public FastThreadLocal() {
        index = InternalThreadLocalMap.nextVariableIndex();
    }

每个 FastThreadLocal 都有一个index,变量从静态全局类 InternalThreadLocalMap 中获取,
InternalThreadLocalMapnextVariableIndex 中获取:

    public static int nextVariableIndex() {
        int index = nextIndex.getAndIncrement();
        if (index < 0) {
            nextIndex.decrementAndGet();
            throw new IllegalStateException("too many thread-local indexed variables");
        }
        return index;
    }

nextIndex 为 AtomicInteger 原子更新类。
所以 FastThreadLocal 很简单,没有寻址过程,每个 FastThreadLocal 都有自己唯一的 index,不会存在hash冲突,真是一个优雅的设计方案!
3. 如果没有找到值,则调用 initialize(threadLocalMap); 方法,初始化一份数据并将本地线程 ,然后填充待删除一些信息:

    private V initialize(InternalThreadLocalMap threadLocalMap) {
        V v = null;
        try {
            v = initialValue();   // 初始化
        } catch (Exception e) {
            PlatformDependent.throwException(e);
        }
        threadLocalMap.setIndexedVariable(index, v);   // 设置值
        addToVariablesToRemove(threadLocalMap, this);   // 记录清理的下标
        return v;
    }

记录完值后,需要记录清理当前 threadLocal的下标,主要用于清理时使用。

    private static void addToVariablesToRemove(InternalThreadLocalMap threadLocalMap, FastThreadLocal<?> variable) {
        Object v = threadLocalMap.indexedVariable(variablesToRemoveIndex);
        Set<FastThreadLocal<?>> variablesToRemove;
        if (v == InternalThreadLocalMap.UNSET || v == null) {
            variablesToRemove = Collections.newSetFromMap(new IdentityHashMap<FastThreadLocal<?>, Boolean>());
            threadLocalMap.setIndexedVariable(variablesToRemoveIndex, variablesToRemove);
        } else {
            variablesToRemove = (Set<FastThreadLocal<?>>) v;
        }
        variablesToRemove.add(variable);
    }
  1. 将该FastThreadLocal信息,封装填充到 variablesToRemoveIndex 位置的 InternalThreadLocalMap 中。
    填充用来干啥呢?
    当然是来 清除时使用的:
    在这里插入图片描述
    清除方式
    有两种清除方式:
  2. 手动执行remove或者removeAll。
  3. netty中,FastThreadLocalRunnable 在run方法中嵌套了逻辑,在执行完后,会调用 FastThreadLocal.removeAll(); 方法进行清理。
    @Override
    public void run() {
        try {
            runnable.run();
        } finally {
            FastThreadLocal.removeAll();
        }
    }

总结

FastThreadLocal 相比于ThreadLoca,有以下优点:

  1. 没有弱引用,不用担心内存泄漏,但是需要手动remove,或执行removeAll方法。
  2. 使用数组替代Map,减少寻址和hash冲突,快!

关注博主公众号: 六点A君。
哈哈哈,一起研究Netty:
在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值