可以看到,根据传入构造 FastThreadLocal 生成的唯一 index 可以直接从 Object 数组里面找到下标并且进行替换,这样一来压根就不会产生冲突,逻辑很简单,完美。
那如果塞入的 value 不是 UNSET(默认值),则执行 addToVariablesToRemove
方法,这个方法又有什么用呢?
这就是 Object 数组的核心关系图了,第一个位置放了一个 set ,set 里面存储了所有使用的 FastThreadLocal 对象,然后数组后面的位置都放 value。
那为什么要放一个 set 保存所有使用的 FastThreadLocal 对象?
用于删除,你想想看,假设现在要清空线程里面的所有 FastThreadLocal ,那必然得有一个地方来存放这些 FastThreadLocal 对象,这样才能找到这些家伙,然后干掉。
所以刚好就把数组的第一个位置腾出来放一个 set 来保存这些 FastThreadLocal 对象,如果要删除全部 FastThreadLocal 对象的时候,只需要遍历这个 set ,得到 FastThreadLocal 的 index 找到数组对应的 位置将 value 置空,然后把 FastThreadLocal 从 set 中移除即可。
刚好 FastThreadLocal 里面实现了这个方法,我们来看下:
内容可能有点多了,我们做下小结,理一理上面说的:
首先 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 的方法。
那这里是什么意思呢?
如果我这个线程之前都没塞过 FastThreadLocal ,此时要塞入第一个 FastThreadLocal ,构造出来的数组长度是32,但是这个 FastThreadLocal 的下标已经涨到了 100 了,所以这个线程第一次塞值,也仅仅只有这么一个值,数组就需要扩容。
看到没,这就是我所说的浪费,空间被浪费了。
Netty 相关实现者知道这样会浪费空间,所以数组的扩容是基于 index 而不是原先数组的大小,你看看如果是基于原先数组的扩容,那么第一次扩容 2 倍,32 变成 64,还是塞不下下标 100 的数据,所以还得扩容一次,这就不美了。
所以可以看到扩容传进去的参数是 index 。
可以看到,直接基于 index 的向上 2 次幂取整。然后就是扩容的拷贝,这里是直接进行数组拷贝,不需要进行 rehash,而 ThreadLocalMap 的扩容需要进行rehash,也就是重新基于 key 的 hash 值进行位置的分配,所以这个也是 FastThreadLocal 优于ThreadLocal 的一个点。
对了,上面那个向上 2 次幂取整的操作,不知道你们熟悉不熟悉,这个和 HashMap 的实现是一致的。
咳咳,但是我没有证据,只能说优秀的代码,就是源远流长。
所以从上面的实现可以得知 Netty 就是特意这样设计的,用多余的空间去换取不会冲突的 set 和 get ,这样写入和获取的速度就更快了,这就是典型的空间换时间。
好了,想必此时你已经弄懂了 FastThreadLocal 的核心原理了,我们再来看看 get 方法的实现,我想你应该能脑补这个实现了。
是吧,没啥难度,index 就是 FastThreadLocal 构造时候预先分配好的那个下标,然后直接进行一个数组下标查找,如果没找到就调用 init 方法进行初始化。
我们这里再继续探究一下InternalThreadLocalMap.get()
,这里面做了一个兼容。不过我要先介绍一下 FastThreadLocalThread ,就是这玩意替代了 Thread。
可以看到它继承了 Thread ,并且弄了一个成员变量就是我们前面说的 InternalThreadLocalMap。
然后我们再来看一下 get 方法,我截了好几个,不过逻辑很简单。