吊打 ThreadLocal,谈谈 FastThreadLocal 为啥能这么快?
这需要从 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
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()
方法看起:
评论