为什么 Netty 不使用 ThreadLocal 而是自定义了一个 FastThreadLocal ?

Sherwin.Wei Lv7

为什么 Netty 不使用 ThreadLocal 而是自定义了一个 FastThreadLocal ?

我们先来思考下 ThreadLocal 有哪些不够好的地方,如果要改都需要改哪些实现。

ThreadLocal hash 冲突的线性探测法不好,还有 Entry 的弱引用可能会发生内存泄漏,这些都和 ThreadLocalMap 有关,所以需要搞个新的 map 来替换 ThreadLocalMap。

而这个 ThreadLocalMap 又是 Thread 里面的一个成员变量,这么一看 Thread 也得动一动,但是我们又无法修改 Thread 的代码,所以配套的还得弄个新的 Thread。

所以我们不仅得弄个新的 ThreadLocal、ThreadLocalMap 还得弄个配套的 Thread 来用上新的 ThreadLocalMap 。

所以如果想改进 ThreadLocal ,就需要动这三个类。

对应到 Netty 的实现就是 FastThreadLocal、InternalThreadLocalMap、FastThreadLocalThread

68747470733a2f2f63646e2e6a7364656c6976722e6e65742f67682f79657373696d6964612f63646e5f696d6167652f696d672f32303232303132333136353332382e706e67.png

然后发散一下思维,既然 Hash 冲突的想线性探测效果不好,你可能比较容易想到的就是上面提到的链表法,然后再基于链表法说个改成红黑树,这个确实是一方面,但是可以再想想。

比如,让 Hash 不冲突,所以设计一个不会冲突的 hash 算法?不存在的!

所以怎么样才不会产生冲突呢?

各自取号入座

什么意思?就是每往 InternalThreadLocalMap 中塞入一个新的 FastThreadLocal 对象,就给这个对象发个唯一的下标,然后让这个对象记住这个下标,到时候去 InternalThreadLocalMap 找 value 的时候,直接通过下标去取对应的 value 。

这样不就不会冲突了?

所以关于 ThreadLocal 的优化 FastThreadLocal 给出了答案

以下 Netty 基于 4.1 版本分析

先来看下 FastThreadLocal 的定义:

68747470733a2f2f63646e2e6a7364656c6976722e6e65742f67682f79657373696d6964612f63646e5f696d6167652f696d672f32303232303132333136353333372e706e67.png 可以看到有个叫 variablesToRemoveIndex 的类成员,并且用 final 修饰的,所以等于每个 FastThreadLocal 都有个共同的不可变 int 值,值为多少等下分析。

然后看到这个 index 没,在 FastThreadLocal 构造的时候就被赋值了,且也被 final 修饰,所以也不可变,这个 index 就是我上面说的给每个新 FastThreadLocal 都发个唯一的下标,这样每个 index 就都知道自己的位置了。

上面两个 index 都是通过 InternalThreadLocalMap.nextVariableIndex() 赋值的,盲猜一下,这个肯定是用原子类递增实现的。

我们来看一下实现:

68747470733a2f2f63646e2e6a7364656c6976722e6e65742f67682f79657373696d6964612f63646e5f696d6167652f696d672f32303232303132333136353334362e706e67.png

在 InternalThreadLocalMap 也定义了一个静态原子类,每次调用 nextVariableIndex 就返回且递增,没有什么别的赋值操作,从这里也可以得知 variablesToRemoveIndex 的值为 0,因为它属于常量赋值,第一次调用时 nextIndex 的值为 0 。

看到这,不知道大家是否已经感觉到一丝不对劲了。好像有点浪费空间的意思,我们继续往下看。

InternalThreadLocalMap 对标的就是之前的 ThreadLocalMap 也就是 ThreadLocal 缺点集中的类,需要重点看下。

我们再来回顾一下 ThreadLocalMap 的定义。

68747470733a2f2f63646e2e6a7364656c6976722e6e65742f67682f79657373696d6964612f63646e5f696d6167652f696d672f32303232303132333136353335362e706e67.png

它是个 Entry 数组,然后 Entry 里面弱引用了 ThreadLocal 作为 Key。

而 InternalThreadLocalMap 有点不太一样:

68747470733a2f2f63646e2e6a7364656c6976722e6e65742f67682f79657373696d6964612f63646e5f696d6167652f696d672f32303232303132333136353430352e706e67.png

可以看到, InternalThreadLocalMap 好像放弃了 map 的形式,没用定义 key 和 value,而是一个 Object 数组?

那它是如何通过 Object 来存储 FastThreadLocal 和对应的 value 的呢?我们从 FastThreadLocal#set 开始分析:

68747470733a2f2f63646e2e6a7364656c6976722e6e65742f67682f79657373696d6964612f63646e5f696d6167652f696d672f32303232303132333136353432312e706e67.png

因为我们已经熟悉 ThreadLocal 的套路,所以我们知道 InternalThreadLocalMap 肯定是 FastThreadLocalThread 里面的一个变量。

然后我们从对应的 FastThreadLocalThread 里面拿到了 map 之后,就要执行塞入操作即 setKnownNotUnset。

我们先看一下塞入操作里面的 setIndexedVariable 方法:

68747470733a2f2f696d672d626c6f672e6373646e696d672e636e2f61363963333566646539373634646461393135346238656264336563323838622e706e67.png

可以看到,根据传入构造 FastThreadLocal 生成的唯一 index 可以直接从 Object 数组里面找到下标并且进行替换,这样一来压根就不会产生冲突,逻辑很简单,完美。

那如果塞入的 value 不是 UNSET(默认值),则执行 addToVariablesToRemove 方法,这个方法又有什么用呢?

image.png

是不是看着有点奇怪?这是啥操作?别急,看我画个图来解释解释:

68747470733a2f2f63646e2e6a7364656c6976722e6e65742f67682f79657373696d6964612f63646e5f696d6167652f696d672f32303232303132333136353434332e706e67.png

这就是 Object 数组的核心关系图了,第一个位置放了一个 set ,set 里面存储了所有使用的 FastThreadLocal 对象,然后数组后面的位置都放 value。

那为什么要放一个 set 保存所有使用的 FastThreadLocal 对象?

用于删除,你想想看,假设现在要清空线程里面的所有 FastThreadLocal ,那必然得有一个地方来存放这些 FastThreadLocal 对象,这样才能找到这些家伙,然后干掉。

所以刚好就把数组的第一个位置腾出来放一个 set 来保存这些 FastThreadLocal 对象,如果要删除全部 FastThreadLocal 对象的时候,只需要遍历这个 set ,得到 FastThreadLocal 的 index 找到数组对应的 位置将 value 置空,然后把 FastThreadLocal 从 set 中移除即可。

刚好 FastThreadLocal 里面实现了这个方法,我们来看下:

68747470733a2f2f63646e2e6a7364656c6976722e6e65742f67682f79657373696d6964612f63646e5f696d6167652f696d672f32303232303132333136353435322e706e67.png

内容可能有点多了,我们做下小结,理一理上面说的:

首先 InternalThreadLocalMap 没有采用 ThreadLocalMap k-v形式的存储方式,而是用 Object 数组来存储 FastThreadLocal 对象和其 value,具体是在第一个位置存放了一个包含所使用的 FastThreadLocal 对象的 set,然后后面存储所有的 value。

之所以需要个 set 是为了存储所有使用的 FastThreadLocal 对象,这样就能找到这些对象,便于后面的删除工作。

之所以数组其他位置可以直接存储 value ,是因为每个 FastThreadLocal 构造的时候已经被分配了一个唯一的下标,这个下标对应的就是 value 所处的下标。

看到这里,不知道大家是否有感受到空间的浪费?

我举个例子。

假设系统里面一个 new 了 100 个 FastThreadLocal ,那第 100 个 FastThreadLocal 的下标就是 100 ,这个应该没有疑义。

从上面的 set 方法可以得知,只有调用 set 的时候,才会从当前线程中拿出 InternalThreadLocalMap ,然后往这个 map 的数组里面塞入 value,这里我们再回顾一下 set 的方法。

68747470733a2f2f63646e2e6a7364656c6976722e6e65742f67682f79657373696d6964612f63646e5f696d6167652f696d672f32303232303132333136353530342e706e67.png

那这里是什么意思呢?

如果我这个线程之前都没塞过 FastThreadLocal ,此时要塞入第一个 FastThreadLocal ,构造出来的数组长度是32,但是这个 FastThreadLocal 的下标已经涨到了 100 了,所以这个线程第一次塞值,也仅仅只有这么一个值,数组就需要扩容。

看到没,这就是我所说的浪费,空间被浪费了。

Netty 相关实现者知道这样会浪费空间,所以数组的扩容是基于 index 而不是原先数组的大小,你看看如果是基于原先数组的扩容,那么第一次扩容 2 倍,32 变成 64,还是塞不下下标 100 的数据,所以还得扩容一次,这就不美了。

所以可以看到扩容传进去的参数是 index 。

68747470733a2f2f63646e2e6a7364656c6976722e6e65742f67682f79657373696d6964612f63646e5f696d6167652f696d672f32303232303132333136353531342e706e67.png

可以看到,直接基于 index 的向上 2 次幂取整。然后就是扩容的拷贝,这里是直接进行数组拷贝,不需要进行 rehash,而 ThreadLocalMap 的扩容需要进行rehash,也就是重新基于 key 的 hash 值进行位置的分配,所以这个也是 FastThreadLocal 优于ThreadLocal 的一个点。

对了,上面那个向上 2 次幂取整的操作,不知道你们熟悉不熟悉,这个和 HashMap 的实现是一致的。

68747470733a2f2f63646e2e6a7364656c6976722e6e65742f67682f79657373696d6964612f63646e5f696d6167652f696d672f32303232303132333136353532332e706e67.png

咳咳,但是我没有证据,只能说优秀的代码,就是源远流长。

所以从上面的实现可以得知 Netty 就是特意这样设计的,用多余的空间去换取不会冲突的 set 和 get ,这样写入和获取的速度就更快了,这就是典型的空间换时间。

好了,想必此时你已经弄懂了 FastThreadLocal 的核心原理了,我们再来看看 get 方法的实现,我想你应该能脑补这个实现了。

68747470733a2f2f63646e2e6a7364656c6976722e6e65742f67682f79657373696d6964612f63646e5f696d6167652f696d672f32303232303132333136353534312e706e67.png

是吧,没啥难度,index 就是 FastThreadLocal 构造时候预先分配好的那个下标,然后直接进行一个数组下标查找,如果没找到就调用 init 方法进行初始化。

我们这里再继续探究一下InternalThreadLocalMap.get(),这里面做了一个兼容。不过我要先介绍一下 FastThreadLocalThread ,就是这玩意替代了 Thread。

68747470733a2f2f63646e2e6a7364656c6976722e6e65742f67682f79657373696d6964612f63646e5f696d6167652f696d672f32303232303132333136353535322e706e67.png

可以看到它继承了 Thread ,并且弄了一个成员变量就是我们前面说的 InternalThreadLocalMap。

然后我们再来看一下 get 方法,我截了好几个,不过逻辑很简单。

68747470733a2f2f63646e2e6a7364656c6976722e6e65742f67682f79657373696d6964612f63646e5f696d6167652f696d672f32303232303132333136353630312e706e67.png

这里之所以分了 fastGet 和 slowGet 是为了做一个兼容,假设有个不熟悉的人,他用了 FastThreadLocal 但是没有配套使用 FastThreadLocalThread ,然后调用 FastThreadLocal#get 的时候去 Thread 里面找 InternalThreadLocalMap 那不就傻了吗,会报错的。

所以就再弄了个 slowThreadLocalMap ,它是个 ThreadLocal ,里面保存 InternalThreadLocalMap 来兼容一下这个情况。

从这里我们也能得知,FastThreadLocal 最好和 FastThreadLocalThread 配套使用,不然就隔了一层了。

1
2
3
4
5
6
7
FastThreadLocal<String> threadLocal = new FastThreadLocal<String>();
Thread t = new FastThreadLocalThread(new Runnable() { //记得要 new FastThreadLocalThread
public void run() {
threadLocal.get();
....
}
});

好了,get 和 set 这两个核心操作都分析完了,我们最后再来看一下 remove 操作吧。

68747470733a2f2f63646e2e6a7364656c6976722e6e65742f67682f79657373696d6964612f63646e5f696d6167652f696d672f32303232303132333136353631322e706e67.png

很简单对吧,把数组里的 value 给覆盖了,然后再到 set 里把对应的 FastThreadLocal 对象给删了。

不过看到这里,可能有人会发出疑惑,内存泄漏相关的点呢?

其实吧,可以看到 FastThreadLocal 就没用弱引用,所以它把无用 FastThreadLocal 的清理就寄托到规范使用上,即没用了就主动调用 remove 方法。

但是它曲线救国了一下,我们来看一下 FastThreadLocalRunnable 这个类:

68747470733a2f2f63646e2e6a7364656c6976722e6e65742f67682f79657373696d6964612f63646e5f696d6167652f696d672f32303232303132333136353632342e706e67.png

我已经把重点画出来了,可以看到这个 Runnable 执行完毕之后,会主动调用 FastThreadLocal.removeAll() 来清理所有的 FastThreadLocal,这就是我说的曲线救国,怕你完了调用 remove ,没事我帮你封装一下,就是这么贴心。

当然,这个前提是你不能用 Runnable 而是用 FastThreadLocalRunnable。不过这里 Netty 也是做了封装的。

Netty 实现了一个 DefaultThreadFactory 工厂类来创建线程。

68747470733a2f2f63646e2e6a7364656c6976722e6e65742f67682f79657373696d6964612f63646e5f696d6167652f696d672f32303232303132333136353633322e706e67.png

你看,你传入 Runnable 是吧,没事,我把它包成 FastThreadLocalRunnable,并且我 new 回去的线程是 FastThreadLocalThread 类型,这样就能在很大程度上避免使用的错误,也减少了使用的难度。

这也是工厂方法这个设计模式的好处之一啦。所以工程上如果怕对方没用对,我们就封装了再给别人使用,这样也屏蔽了一些细节,他好你也好。

所以说多看看开源框架的源码,有很多可以学习的地方!好了,FastThreadLocal 原理大致就说到这里。

Comments
On this page
为什么 Netty 不使用 ThreadLocal 而是自定义了一个 FastThreadLocal ?