作者 | joel.wang老王
既然jdk已经有threadlocal,为何netty还要自己造个fastthreadlocal?fastthreadlocal快在哪里?
这需要从jdk threadlocal的本身说起。如下图:
在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>,如下图:
根据上文图示可知,ftl的实现,涉及到internalthreadlocalmap、fastthreadlocalthread和fastthreadlocal几个类,自底向上,我们先从internalthreadlocalmap开始分析。
internalthreadlocalmap类的继承关系图如下:
数组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集合中。
此处不贴代码,用图示如下:
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获取指定数组下标的值,对象关系示意图:
在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数据库
聊聊前后端分离的接口规范