写点什么

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

  • 2021 年 11 月 11 日
  • 本文字数:3589 字

    阅读完需:约 12 分钟

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



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


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


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


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



2 实现源码分析




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


InternalThreadLocalMap 类的继承关系图如下:


2.1 UnpaddedInternalThreadLocalMap 的主要属性

static?final?ThreadLocal<InternalThreadLocalMap>?slowThreadLocalMap?=?new?ThreadLocal<InternalThreadLocalMap>();


static?final?AtomicInteger?nextIndex?=?new?AtomicInteger();


Object[]?indexedVariables;


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

2.2 InternalThreadLocalMap 分析

InternalThreadLocalMap 的主要属性:


//?用于标识数组的槽位还未使用


public?static?final?Object?UNSET?=?new?Object();


/**


*?用于标识 ftl 变量是否注册了 cleaner


  • BitSet 简要原理:

  • BitSet 默认底层数据结构是一个 long[]数组,开始时长度为 1,即只有 long[0],而一个 long 有 64bit。


*?当 BitSet.set(1)的时候,表示将 long[0]的第二位设置为 true,即 0000?0000?...?0010(64bit),则 long[0]==2


*?当 BitSet.get(1)的时候,第二位为 1,则表示 true;如果是 0,则表示 false


*?当 BitSet.set(64)的时候,表示设置第 65 位,此时 long[0]已经不够用了,扩容处 long[1]来,进行存储



    *?存储类似?{index:boolean}?键值对,用于防止一个 FastThreadLocal 多次启动清理线程


    *?将 index 位置的 bit 设为 true,表示该 InternalThreadLocalMap 中对该 FastThreadLocal 已经启动了清理线程


    */


    private?BitSet?cleanerFlags;?


    private?InternalThreadLocalMap()?{


    super(newIndexedVariableTable());


    }


    private?static?Object[]?newIndexedVariableTable()?{


    Object[]?array?=?ne


    【一线大厂Java面试题解析+后端开发学习笔记+最新架构讲解视频+实战项目源码讲义】
    浏览器打开:qq.cn.hn/FTf 免费领取
    复制代码


    w?Object[32];


    Arrays.fill(array,?UNSET);


    return?array;


    }


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


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

    2.3 ftlt 的实现分析

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


    public?class?FastThreadLocalThread?extends?Thread?{


    //?This?will?be?set?to?true?if?we?have?a?chance?to?wrap?the?Runnable.


    private?final?boolean?cleanupFastThreadLocals;


    private?InternalThreadLocalMap?threadLocalMap;


    public?final?InternalThreadLocalMap?threadLocalMap()?{


    return?threadLocalMap;


    }


    public?final?void?setThreadLocalMap(InternalThreadLocalMap?threadLocalMap)?{


    this.threadLocalMap?=?threadLocalMap;


    }


    }??


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

    2.4 ftl 实现分析

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

    2.4.1 ftl 的属性和实例化

    private?final?int?index;


    public?FastThreadLocal()?{


    index?=?InternalThreadLocalMap.nextVariableIndex();


    }


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


    public?static?int?nextVariableIndex()?{


    int?index?=?nextIndex.getAndIncrement();


    if?(index?<?0)?{


    nextIndex.decrementAndGet();


    throw?new?IllegalStateException("too?many?thread-local?indexed?variables");


    }


    return?index;


    }


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

    2.4.2 get()方法实现分析

    public?final?V?get()?{


    InternalThreadLocalMap?threadLocalMap?=?InternalThreadLocalMap.get();?//?1


    Object?v?=?threadLocalMap.indexedVariable(index);?//?2


    if?(v?!=?InternalThreadLocalMap.UNSET)?{


    return?(V)?v;


    }


    V?value?=?initialize(threadLocalMap);?//?3


    registerCleaner(threadLocalMap);??//?4


    return?value;


    }


    1.先来看看InternalThreadLocalMap.get()方法如何获取 threadLocalMap:


    =======================InternalThreadLocalMap=======================??


    public?static?InternalThreadLocalMap?get()?{


    Thread?thread?=?Thread.currentThread();


    if?(thread?instanceof?FastThreadLocalThread)?{


    return?fastGet((FastThreadLocalThread)?thread);


    }?else?{


    return?slowGet();


    }


    }


    private?static?InternalThreadLocalMap?fastGet(FastThreadLocalThread?thread)?{


    InternalThreadLocalMap?threadLocalMap?=?thread.threadLocalMap();


    if?(threadLocalMap?==?null)?{


    thread.setThreadLocalMap(threadLocalMap?=?new?InternalThreadLocalMap());


    }


    return?threadLocalMap;


    }????


    因为结合 FastThreadLocalThread 使用才能发挥 FastThreadLocal 的性能优势,所以主要看 fastGet 方法。该方法直接从 ftlt 线程获取 threadLocalMap,还没有则创建一个 InternalThreadLocalMap 实例并设置进去,然后返回。学习资料:Java进阶视频资源


    2.threadLocalMap.indexedVariable(index)就简单了,直接从数组获取值,然后返回:


    public?Object?indexedVariable(int?index)?{


    Object[]?lookup?=?indexedVariables;


    return?index?<?lookup.length??lookup[index]?:?UNSET;


    }


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


    initialize(threadLocalMap)方法:


    private?V?initialize(InternalThreadLocalMap?threadLocalMap)?{


    V?v?=?null;


    try?{


    v?=?initialValue();


    }?catch?(Exception?e)?{


    PlatformDependent.throwException(e);


    }


    threadLocalMap.setIndexedVariable(index,?v);?//?3-1


    addToVariablesToRemove(threadLocalMap,?this);?//?3-2


    return?v;


    }


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


    3.2.addToVariablesToRemove(threadLocalMap, this)的实现,是将 ftl 实例保存在 threadLocalMap 内部数组第 0 个元素的 Set 集合中。


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



    4.registerCleaner(threadLocalMap)的实现,netty-4.1.34 版本中的源码:


    private?void?registerCleaner(final?InternalThreadLocalMap?threadLocalMap)?{


    Thread?current?=?Thread.currentThread();


    if?(FastThreadLocalThread.willCleanupFastThreadLocals(current)?||?threadLocalMap.isCleanerFlagSet(index))?{


    return;


    }


    threadLocalMap.setCleanerFlag(index);


    //?TODO:?We?need?to?find?a?better?way?to?handle?this.


    /*


    //?We?will?need?to?ensure?we?will?trigger?remove(InternalThreadLocalMap)?so?everything?will?be?released


    //?and?FastThreadLocal.onRemoval(...)?will?be?called.


    ObjectCleaner.register(current,?new?Runnable()?{


    @Override


    public?void?run()?{


    remove(threadLocalMap);


    //?It's?fine?to?not?call?InternalThreadLocalMap.remove()?here?as?this?will?only?be?triggered?once


    //?the?Thread?is?collected?by?GC.?In?this?case?the?ThreadLocal?will?be?gone?away?already.


    }


    });


    */


    }


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

    2.5 普通线程使用 ftl 的性能退化

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


    前文说过,ftl 要结合 ftlt 才能最大地发挥其性能,如果是其他的普通线程,就会退化到 jdk 的 ThreadLocal 的情况,因为普通线程没有包含 InternalThreadLocalMap 这样的数据结构,接下来我们看如何退化。学习资料:Java进阶视频资源


    从 InternalThreadLocalMap 的get()方法看起:

    评论

    发布
    暂无评论
    吊打 ThreadLocal,谈谈FastThreadLocal为啥能这么快?