天天看点

吊打 ThreadLocal,谈谈FastThreadLocal为啥能这么快?

吊打 ThreadLocal,谈谈FastThreadLocal为啥能这么快?

作者 | joel.wang老王

既然jdk已经有threadlocal,为何netty还要自己造个fastthreadlocal?fastthreadlocal快在哪里?

这需要从jdk threadlocal的本身说起。如下图:

吊打 ThreadLocal,谈谈FastThreadLocal为啥能这么快?

在java线程中,每个线程都有一个threadlocalmap实例变量(如果不使用threadlocal,不会创建这个map,一个线程第一次访问某个threadlocal变量时,才会创建)。

该map是使用线性探测的方式解决hash冲突的问题,如果没有找到空闲的slot,就不断往后尝试,直到找到一个空闲的位置,插入entry,这种方式在经常遇到hash冲突时,影响效率。

fastthreadlocal(下文简称ftl)直接使用数组避免了hash冲突的发生,具体做法是:每一个fastthreadlocal实例创建时,分配一个下标index;分配index使用atomicinteger实现,每个fastthreadlocal都能获取到一个不重复的下标。

当调用​<code>​ftl.get()​</code>​方法获取值时,直接从数组获取返回,如​<code>​return array[index]​</code>​,如下图:

吊打 ThreadLocal,谈谈FastThreadLocal为啥能这么快?

根据上文图示可知,ftl的实现,涉及到internalthreadlocalmap、fastthreadlocalthread和fastthreadlocal几个类,自底向上,我们先从internalthreadlocalmap开始分析。

internalthreadlocalmap类的继承关系图如下:

吊打 ThreadLocal,谈谈FastThreadLocal为啥能这么快?

数组indexedvariables就是用来存储ftl的value的,使用下标的方式直接访问。nextindex在ftl实例创建时用来给每个ftl实例分配一个下标,slowthreadlocalmap在线程不是ftlt时使用到。

internalthreadlocalmap的主要属性:

比较简单,​<code>​newindexedvariabletable()​</code>​方法创建长度为32的数组,然后初始化为unset,然后传给父类。之后ftl的值就保存到这个数组里面。

注意,这里保存的直接是变量值,不是entry,这是和jdk threadlocal不同的。internalthreadlocalmap就先分析到这,其他方法在后面分析ftl再具体说。

要发挥ftl的性能优势,必须和ftlt结合使用,否则就会退化到jdk的threadlocal。ftlt比较简单,关键代码如下:

ftlt的诀窍就在threadlocalmap属性,它继承java thread,然后聚合了自己的internalthreadlocalmap。后面访问ftl变量,对于ftlt线程,都直接从internalthreadlocalmap获取变量值。

ftl实现分析基于netty-4.1.34版本,特别地声明了版本,是因为在清除的地方,该版本的源码已经注释掉了objectcleaner的调用,和之前的版本有所不同。

非常简单,就是给属性index赋值,赋值的静态方法在internalthreadlocalmap:

可见,每个ftl实例以步长为1的递增序列,获取index值,这保证了internalthreadlocalmap中数组的长度不会突增。

1.先来看看​<code>​internalthreadlocalmap.get()​</code>​方法如何获取threadlocalmap:

因为结合fastthreadlocalthread使用才能发挥fastthreadlocal的性能优势,所以主要看fastget方法。该方法直接从ftlt线程获取threadlocalmap,还没有则创建一个internalthreadlocalmap实例并设置进去,然后返回。

2.​<code>​threadlocalmap.indexedvariable(index)​</code>​就简单了,直接从数组获取值,然后返回:

3.如果获取到的值不是unset,那么是个有效的值,直接返回。如果是unset,则初始化。

​<code>​initialize(threadlocalmap)​</code>​方法:

3.1.获取ftl的初始值,然后保存到ftl里的数组,如果数组长度不够则扩充数组长度,然后保存,不展开。

3.2.​<code>​addtovariablestoremove(threadlocalmap, this)​</code>​的实现,是将ftl实例保存在threadlocalmap内部数组第0个元素的set集合中。

此处不贴代码,用图示如下:

吊打 ThreadLocal,谈谈FastThreadLocal为啥能这么快?

4.​<code>​registercleaner(threadlocalmap)​</code>​的实现,netty-4.1.34版本中的源码:

由于objectcleaner.register这段代码在该版本已经注释掉,而余下逻辑比较简单,因此不再做分析。

随着​<code>​get()​</code>​方法分析完毕,​<code>​set(value)​</code>​方法原理也呼之欲出,限于篇幅,不再单独分析。

前文说过,ftl要结合ftlt才能最大地发挥其性能,如果是其他的普通线程,就会退化到jdk的threadlocal的情况,因为普通线程没有包含internalthreadlocalmap这样的数据结构,接下来我们看如何退化。

从internalthreadlocalmap的​<code>​get()​</code>​方法看起:

从ftl看,退化操作的整个流程是:从一个jdk的threadlocal变量中获取internalthreadlocalmap,然后再从internalthreadlocalmap获取指定数组下标的值,对象关系示意图:

吊打 ThreadLocal,谈谈FastThreadLocal为啥能这么快?

在netty中对于ftl提供了三种回收机制:

自动: 使用ftlt执行一个被fastthreadlocalrunnable wrap的runnable任务,在任务执行完毕后会自动进行ftl的清理。

手动: ftl和internalthreadlocalmap都提供了remove方法,在合适的时候用户可以(有的时候也是必须,例如普通线程的线程池使用ftl)手动进行调用,进行显示删除。

自动: 为当前线程的每一个ftl注册一个cleaner,当线程对象不强可达的时候,该cleaner线程会将当前线程的当前ftl进行回收。(netty推荐如果可以用其他两种方式,就不要再用这种方式,因为需要另起线程,耗费资源,而且多线程就会造成一些资源竞争,在netty-4.1.34版本中,已经注释掉了调用objectcleaner的代码。)

ftl在netty中最重要的使用,就是分配bytebuf。基本做法是:每个线程都分配一块内存(poolarena),当需要分配bytebuf时,线程先从自己持有的poolarena分配,如果自己无法分配,再采用全局分配。

但是由于内存资源有限,所以还是会有多个线程持有同一块poolarena的情况。不过这种方式已经最大限度地减轻了多线程的资源竞争,提高程序效率。

具体的代码在poolbytebufallocator的内部类poolthreadlocalcache中:

参考资料

netty源码分析3 - fastthreadlocal 框架的设计

netty进阶:自顶向下解析fastthreadlocal

往期推荐

​​spring boot实现定时任务的动态增删启停​​

​​你在 docker 中跑 mysql?恭喜你,可以下岗了!​​

​​0.2秒居然复制了100g文件?​​

​​spring boot中使用postgresql数据库​​

​​聊聊前后端分离的接口规范​​