本文转载自微信公众号《是的练级攻略》,作者是是的。转载本文请联系yes的练级指导公众号。你好,我是。上一篇之后,ThreadLocal能问的问题我都写完了。让我们有一个FastThreadLocal的磁盘。这是ThreadLocal的高级版本。它是Netty为ThreadLocal本身做的一个轮子。阅读上一篇文章,打下基础。了解了FastThreadLocal之后,平日的一些优化可能会提供一些思路,也可以在面试的时候装x。面试官:ThreadLocal其实有xxx的劣势,那怎么优化呢?FastThreadLocal的BB你实现一次就可以了,不安全吗!那么,今天我们就来看看Netty是如何实现FastThreadLocal的,话不多说,本文提纲如下:盘点ThreadLocal的缺点。ThreadLocal的缺点应该如何改进呢?FastThreadLocal的原理。FastThreadLocalVSThreadLocal的实战操作。看完这篇,高级版的ThreadLocal基本拿下了。下一篇我会在这篇文章的基础上做一个扩展,一个比较底层的扩展,属于绝对安装x的那种。看完文章你就知道了,我就埋了吧,哈哈。作为预览,这篇文章很长,源代码有点多,但耐心看完一定会有收获。开始!盘点ThreadLocal的缺点看过上一篇文章的同学应该很清楚ThreadLocal的一个缺点:hash冲突采用线性检测方式,效率低下。可以看出,图中显示经过两次遍历,已经找到了空位。如果有更多的冲突,则需要更多的遍历。而下次拿到的时候,发现hash直接命中的位置不是你要找的Entry,所以还要继续遍历,向后找,这样效率低。HashMap采用链表的方式来解决冲突,并且为了防止遍历链表的开销变大,会在一定条件后转成红黑树进行查找。这样的解决方案肯定比线性探测更好,所以这是一个优化方向。但是FastThreadLocal并没有这样优化,下面再说。还有一个缺点就是ThreadLocal使用了WeakReference来保证资源可以被释放,但这可能会导致一些Etnry键为null,即无用的Entry存在。因此,在调用ThreadLocal的get或set方法时,会主动清理无用的Entry,减少内存泄漏的发生。这实际上相当于把清理开销放在了get和set上。如果get的时候清理了很多无用的entry,那么这次get会比较慢。还有内存泄漏的问题。当然这个问题只有在使用线程池的时候才会存在,而且上面也提到了get和set也可以清理一些无用的key,所以没有那么夸张。只要记得使用调用ThreadLocal#remove后,就不会出现内存泄漏问题。就是这样。如何改善ThreadLocal的缺点?那么如何改变呢?前面提到ThreadLocalhash冲突的线性检测方式不好,Entry的弱引用可能会导致内存泄漏。这些都和ThreadLocalMap有关,所以需要一个新的map。替换ThreadLocalMap。而这个ThreadLocalMap是Thread中的一个成员变量,所以Thread是要移动的,但是我们又不能修改Thread的代码,所以要新建一个Thread进行匹配。所以我们不仅要得到一个新的ThreadLocal、ThreadLocalMap,还要有一个匹配的Thread来使用这个新的ThreadLocalMap。所以如果你想改进ThreadLocal,你需要移动这三个类。Netty对应的实现是FastThreadLocal、InternalThreadLocalMap、FastThreadLocalThread,再发散一下思路。由于Hash冲突的线性检测效果不好,大家可能会想到上面提到的链表法,然后在链表法的基础上改一下。红黑树,这确实是一方面,但是大家可以再想一想。比如让Hash不冲突,那么设计一个不冲突的hash算法?它不存在!那么怎么可能没有冲突呢?各取号坐是什么意思?就是每次往InternalThreadLocalMap中插入一个新的FastThreadLocal对象,只是给这个对象发送一个唯一的下标,然后让这个对象记住这个下标。去InternalThreadLocalMap找值的时候,直接用下标就可以得到对应的值。不会有冲突吗?这是FastThreadLocal给出的解决方案,下面会详细分析。还有内存泄漏的问题。其实只要规范使用,用完就可以去掉。其实也没有什么好的解决办法,不过FastThreadLocal曲线救国了。这还要看下面的分析!FastThreadLocal的原理下面基于Netty4.1版本的分析首先看一下FastThreadLocal的定义:可以看到有一个类成员variablesToRemoveIndex,用final修饰,也就是说每个FastThreadLocal都有一个共同的immutableint值,值是多少等。接下来分析。然后看到索引是在构造FastThreadLocal的时候赋值的,也是final修改的,所以也是不可更改的。这个index就是我上面说的给每一个new的FastThreadLocal发送一个唯一的下标,让每个everyindex都知道你在哪。上面两个索引是通过InternalThreadLocalMap.nextVariableIndex()赋值的,瞎猜,这一定是通过类原子自增来实现的。我们看一下实现:确实,InternalThreadLocalMap中也定义了一个静态原子类,每次调用nextVariableIndex都会返回并自增,没有其他任何赋值操作。从这里我们也可以知道variablesToRemoveIndex的值为0,因为是常量Assignment,第一次调用时nextIndex的值为0。看到这里,不知道大家有没有觉得不对劲。好像有点浪费篇幅,我们继续往下看。InternalThreadLocalMap针对的是之前的ThreadLocalMap,这是ThreadLocal缺点最多的类,所以需要重点关注。我们再回顾一下ThreadLocalMap的定义。是一个Entry的数组,ThreadLocal在Entry中被弱引用为Key。但是InternalThreadLocalMap有点不一样:如你所见,InternalThreadLocalMap似乎放弃了map的形式,而是定义了key和value,而是一个Object数组?那么它是如何通过Object存储FastThreadLocal和对应的值的呢?我们从FastThreadLocal#set开始分析:因为我们已经熟悉了ThreadLocal的例程,所以我们知道InternalThreadLocalMap一定是FastThreadLocalThread中的一个变量。那么我们从对应的FastThreadLocalThread中拿到map后,需要进行填充操作,即setKnownNotUnset。我们先看一下insert操作中的setIndexedVariable方法:可以看到传入构造FastThreadLocal生成的唯一索引可以直接从Object数组中找到下标并替换,这样就完全不会冲突了,而且逻辑非常简单和完美。然后如果插入的值不是UNSET(默认值),则执行addToVariablesToRemove方法。这个方法有什么用?是不是看起来有点奇怪?这是什么操作?别着急,我画个图来解释一下:这就是Object数组的核心关系图。第一个位置放一个set,所有用到的FastThreadLocal对象都存放在set中,values放在数组后面的位置。那为什么要放一个set来保存所有使用过的FastThreadLocal对象呢?对于删除,想一想,如果现在要清除线程中的所有FastThreadLocal,就必须有一个地方存放这些FastThreadLocal对象,这样才能找到这些家伙,然后干掉。所以就把数组的第一个位置空出来,放一个set来保存这些FastThreadLocal对象。如果想删除所有的FastThreadLocal对象,只需要遍历这个集合,得到FastThreadLocal的索引,找到数组对应的位置,将值置空即可。然后从集合中移除FastThreadLocal。正好这个方法是在FastThreadLocal中实现的。来看一下:图片的内容可能有点多。总结一下上面的理解:首先,InternalThreadLocalMap并没有使用ThreadLocalMap的k-v形式存储,而是使用Object数组来存储FastThreadLocal对象及其值,具体来说就是在第一个位置存储一个包含使用过的FastThreadLocal对象的集合,然后所有的值都存储在后面。之所以需要一个集合,是为了存放所有使用过的FastThreadLocal对象,方便以后删除时找到这些对象。之所以可以直接将value存放在数组的其他位置,是因为每个FastThreadLocal在构造的时候都被赋予了一个唯一的下标,这个下标对应着value所在的下标。看到这里,不知道大家有没有觉得浪费空间呢?让我举一个例子。假设系统中新增了100个FastThreadLocal,第100个FastThreadLocal的下标为100,这一点应该是没有疑问的。从上面的set方法我们可以知道,只有调用set的时候,InternalThreadLocalMap才会从当前线程中取出来,然后将值插入到这个map的数组中。这里我们再回顾一下set方法。那么这是什么意思?如果我之前没有在这个线程里面塞过FastThreadLocal,这时候想塞第一个FastThreadLocal,构造出来的数组长度是32,但是这个FastThreadLocal的下标已经增加到100了,所以这个线程第一次value被填充,就只有这么一个值,需要扩充数组。看,这就是我所说的浪费,空间被浪费了。Netty相关的实现者知道这样会浪费空间,所以数组的扩充是根据索引而不是原数组的大小。可以看到如果在原数组的基础上扩容,那么第一次扩容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方法。我砍了几个,但逻辑很简单。这里之所以把fastGet和slowGet分开,是为了兼容。假设有一个不熟悉的人使用了FastThreadLocal但没有使用FastThreadLocalThread。然后在调用FastThreadLocal#get的时候,去Thread里面找InternalThreadLocalMap。会不会很傻?,会报错。所以我又得到了一个slowThreadLocalMap,它是一个ThreadLocal,它存储了InternalThreadLocalMap以兼容这种情况。从这里我们也可以知道,FastThreadLocal最好和FastThreadLocalThread配合使用,否则会隔一层。FastThreadLocal
