你好,我是yes。
继上一篇之后我把 ThreadLocal 能问的,都写了之后,咱们再来盘一盘 FastThreadLocal ,这个算是 ThreadLocal 的进阶版,是 Netty 针对 ThreadLocal 自己造的轮子,所以对 ThreadLocal 没有完全理解的话,建议先看上一篇文章,打个基础。
那了解 FastThreadLocal 之后呢,对平日的一些优化可能可以提供一些思路,或者面试就能装个x。
面试官:ThreadLocal 竟然有xxx这个缺点,那怎么优化啊?
你就把 FastThreadLocal 的实现 BB 一遍,这不就稳妥了嘛!
所以,今天我们就来看看 Netty 是如何实现 FastThreadLocal 的,话不多说,本文大纲如下:
- 数数 ThreadLocal 的缺点。
- 应该如何针对 ThreadLocal 缺点改进?
- FastThreadLocal 的原理。
- FastThreadLocal VS ThreadLocal 的实操。
这篇下来,进阶版 ThreadLocal 基本拿下,下篇我会基于这篇做一个延伸,一个比较底层的延伸,属于绝对装x的那种,等下看文章你就知道了,我会埋坑的,哈哈。
发车发车!
数数 ThreadLocal 的缺点
看完上篇文章的同学,应该都很清楚了 ThreadLocal 的一个缺点:hash 冲突用的是线性探测法,效率低。
可以看到,图上显示的是经过两个遍历找到了空位,假设冲突多了,需要遍历的次数就多了。并且下次 get 的时候,hash 直接命中的位置发现不是要找的 Entry ,于是就接着遍历向后找,所以说这个效率低。
而像 HashMap 是通过链表法来解决冲突,并且为了防止链表过长遍历的开销变大,在一定条件之后又会转变成红黑树来查找,这样的解决方案在频繁冲突的条件下,肯定是优于线性探测法,所以这是一个优化方向。
不过 FastThreadLocal 不是这样优化的,我们下面再说。
还有一个缺点是 ThreadLocal 使用了 WeakReference 以保证资源可以被释放,但是这可能会产生一些 Etnry 的 key 为 null,即无用的 Entry 存在。
所以调用 ThreadLocal 的 get 或 set 方法时,会主动清理无用的 Entry,减轻内存泄漏的发生。
这其实等于把清理的开销弄到了 get 和 set 上,万一 get 的时候清理的无用 Entry 特别多,那这次 get 相对而言就比较慢了。
还有一个就是内存泄漏的问题了,当然这个问题只存在于用线程池使用的时候,并且上面也提到了 get 和 set 的时候也能清理一些无用的 Key,所以没有那么的夸张,只要记得用完后调用 ThreadLocal#remove 就不会有内存泄漏的问题了。
大致就这么几点。
应该如何针对 ThreadLocal 缺点改进
所以怎么改呢?
前面提到 ThreadLocal hash 冲突的线性探测法不好,还有 Entry 的弱引用可能会发生内存泄漏,这些都和 ThreadLocalMap 有关,所以需要搞个新的 map 来替换 ThreadLocalMap。
而这个 ThreadLocalMap 又是 Thread 里面的一个成员变量,这么一看 Thread 也得动一动,但是我们又无法修改 Thread 的代码,所以配套的还得弄个新的 Thread。
所以我们不仅得弄个新的 ThreadLocal、ThreadLocalMap 还得弄个配套的 Thread 来用上新的 ThreadLocalMap 。
所以如果想改进 ThreadLocal ,就需要动这三个类。
对应到 Netty 的实现就是 FastThreadLocal、InternalThreadLocalMap、FastThreadLocalThread
然后发散一下思维,既然 Hash 冲突的想线性探测效果不好,你可能比较容易想到的就是上面提到的链表法,然后再基于链表法说个改成红黑树,这个确实是一方面,但是可以再想想。
比如,让 Hash 不冲突,所以设计一个不会冲突的 hash 算法?不存在的!
所以怎么样才不会产生冲突呢?
各自取号入座
什么意思?就是每往 InternalThreadLocalMap 中塞入一个新的 FastThreadLocal 对象,就给这个对象发个唯一的下标,然后让这个对象记住这个下标,到时候去 InternalThreadLocalMap 找 value 的时候,直接通过下标去取对应的 value 。
这样不就不会冲突了?
这就是 FastThreadLocal 给出的方案,具体下面分析。
还有个内存泄漏的问题,这个其实只要规范的使用即用完后 remove 就好了,其实也没太好的解决方案,不过 FastThreadLocal 曲线救国了一下,这个也且看下面的分析!
FastThreadLocal 的原理
以下 Netty 基于 4.1 版本分析
先来看下 FastThreadLocal 的定义:
可以看到有个叫 variablesToRemoveIndex 的类成员,并且用 final 修饰的,所以等于每个 FastThreadLocal 都有个共同的不可变 int 值,值为多少等下分析。
然后看到这个 index 没,在 FastThreadLocal 构造的时候就被赋值了,且也被 final 修饰,所以也不可变,这个 index 就是我上面说的给每个新 FastThreadLocal 都发个唯一的下标,这样每个 index 就都知道自己的位置了。
上面两个 index 都是通过 InternalThreadLocalMap.nextVariableIndex()
赋值的,盲猜一下,这个肯定是用原子类递增实现的。
我们来看一下实现:
确实,在 InternalThreadLocalMap 也定义了一个静态原子类,每次调用 nextVariableIndex 就返回且递增,没有什么别的赋值操作,从这里也可以得知 variablesToRemoveIndex 的值为 0,因为它属于常量赋值,第一次调用时 nextIndex 的值为 0 。
看到这,不知道大家是否已经感觉到一丝不对劲了。好像有点浪费空间的意思,我们继续往下看。
InternalThreadLocalMap 对标的就是之前的 ThreadLocalMap 也就是 ThreadLocal 缺点集中的类,需要重点看下。
我们再来回顾一下 ThreadLocalMap 的定义。
它是个 Entry 数组,然后 Entry 里面弱引用了 ThreadLocal 作为 Key。
而 InternalThreadLocalMap 有点不太一样:
可以看到, InternalThreadLocalMap 好像放弃了 map 的形式,没用定义 key 和 value,而是一个 Object 数组?
那它是如何通过 Object 来存储 FastThreadLocal 和对应的 value 的呢?我们从 FastThreadLocal#set 开始分析:
因为我们已经熟悉 ThreadLocal 的套路,所以我们知道 InternalThreadLocalMap 肯定是 FastThreadLocalThread 里面的一个变量。
然后我们从对应的 FastThreadLocalThread 里面拿到了 map 之后,就要执行塞入操作即 setKnownNotUnset
。
我们先看一下塞入操作里面的 setIndexedVariable
方法: