这是我参与2022首次更文挑战的第21天,活动详情查看:2022首次更文挑战
一、背景
在之前的三篇文章中,研究了可以通过InheritableThreadLocal进行父子线程中如何传递本地线程变量,通过阿里开源项目TransmitableThreadLocal进行进行线程池传递本地线程变量(详解可查看以往博客)。在查找资料的过程中无意发现了Dobbo中定义了新的InternalThreadLocal,其实Dobbo的InternalThreadLocal和netty的FastThreadLocal有异曲同工之妙。之前学netty的时候有了解一点,为了加深一下ThreadLocal种群的了解,使用本博客记录一下。
二、实例
简单测试:分别使用原生线程调用方式和使用FastThreadLocalThread线程调用方式(netty的FastThreadLocal和阿里的ttl类似,要借助于FastThreadLocalThread结合进行使用。)
public static void main(String[] args) {
new Thread(new Runnable() {
@Override
public void run() {
username.set("zhangSang");
log.info(username.get());
}
}).start();
new FastThreadLocalThread(new Runnable() {
@Override
public void run() {
username.set("zhangSang");
log.info(username.get());
}
}).start();
}
二者的输出结果是一致,所以单从运行结果看,都能获取到对应设置的值,二者没有任何输出区别,但是跟踪一下,可以看到调用的逻辑是有所区别的。
三、原理
熟悉ThreadLocal的应该知道,Threadlocal其实内部逻辑是一个以ThreadLocal对象为key,需要存储的值为value的map结构。而FastThreadLocal的内部实现是一个数组,实现上是直接通过下标定位元素,所以单纯从取、存的角度看,FastThreadLocal是比ThreadLocal高效。
对于使用原生线程Thread来说,其实最后是将数据存Thread.threadLocals(ThreadLocal.ThreadLocalMap)中,也就是说在这个线程所使用的所有FastThreadLocal最后都以 key=ThreadLocal<InternalThreadLocalMap>
对象,value=InternalThreadLocalMap的方式存在线程ThreadLocal的一个节点中。而若使用的是netty封装的FastThreadLocalThread,则FastThreadLocalThread对象的属性threadLocalMap中。
FastThreadLocalThread是直接继承于线程类Thread,并且内部维护一个InternalThreadLocalMap,用于存储变量。虽然这个类命名为Map,结构上其实是一个数组。并且下标为0的元素是一个Set<FastThreadLocal<?>>的结构,存储着当前有效的FastThreadLocal。
public class FastThreadLocalThread extends Thread{
private InternalThreadLocalMap threadLocalMap;
}
在InternalThreadLocalMap中提供了一些静态方法,通过当前线程的不同类型,以不同的方式获取对应所需要的InternalThreadlocalMap。进入FastThreadLocalThread的获取方法,这里可以直接看到,这里判断是线程类型是否是FastThreadLocalThread,如果是,则采用快速获取的方式,如果不是,则采用jdk原生的获取方式。
public static InternalThreadLocalMap get() {
Thread thread = Thread.currentThread();
if (thread instanceof FastThreadLocalThread) {
return fastGet((FastThreadLocalThread) thread);
} else {
return slowGet();
}
}
快速获取方式:直接从FastThreadLocalThread对象中获取InternalThreadLocalMap,如果这个线程变量map为空,则创建一个内置InternalThreadLocalMap对象后返回。
private static InternalThreadLocalMap fastGet(FastThreadLocalThread thread) {
if (threadLocalMap == null) {
thread.setThreadLocalMap(threadLocalMap = new InternalThreadLocalMap());
}
// ..
}
原生线程的获取方法:从UnpaddedInternalThreadLocalMap中获取到ThreadLocal<InternalThreadLocalMap>在从ThreadLocal中获取InternalThreadLocalMap,若为空初始化一个。所以由此可知,原生线程的FastThreadLocal具体值,是以InternalThreadLocalMap为值,存储在ThreadLocal中。
private static InternalThreadLocalMap slowGet() {
ThreadLocal<InternalThreadLocalMap> slowThreadLocalMap =
UnpaddedInternalThreadLocalMap.slowThreadLocalMap;
InternalThreadLocalMap ret = slowThreadLocalMap.get();
if (ret == null) {
ret = new InternalThreadLocalMap();
slowThreadLocalMap.set(ret);
}
return ret;
}
所以从以上两种获取方式可以得出,通过FastThreadLocalThread包装线程创建的线程对象,保存线程变量的map是InternalThreadLocalMap。
1.set值
根据不同的线程类型,获取到的InternalThreadLocalMap进行设置变量值。这里有一个占位判断,如果value值不等于占位值,将需要存储的值添加到InternalThreadLocalMap对应的下标位置。
public final void set(V value) {
if (value != InternalThreadLocalMap.UNSET) {
set(InternalThreadLocalMap.get(), value);
}
}
public final void set(InternalThreadLocalMap threadLocalMap, V value) {
if (value != InternalThreadLocalMap.UNSET) {
if (threadLocalMap.setIndexedVariable(index, value)) {
addToVariablesToRemove(threadLocalMap, this);
}
}
}
最后的添加逻辑:根据下标获取对应的元素,再判断获取到的结果值是否为空,空则同时创建set结构并将值添加到对应的下标位置,否者否则直接应用取出的值,最后将原始添加到set集合中。
private static void addToVariablesToRemove() {
// ...
}
2.get值
获取对应变量值:获取的操作比较简单,直接从threadLocalMap中获取对应的下标元素直接返回。如果等于UNSET占位符则进入初始化map的流程。是一个默认值操作,可以通过最后的initValue方法会调用FastThreadLocal#initialValue做一个初始化的操作。
public final V get(InternalThreadLocalMap threadLocalMap) {
// ...
if (v != InternalThreadLocalMap.UNSET) {
return (V) v;
}
return initialize(threadLocalMap);
}
3.remove方法
移除变量值:删除对应下标值,赋值为UNSET占位符,删除InternalThreadLocalMap[0]的Set<FastThreadLocal<?>>中的当前FastThreadLocal对象,这里有提供一个扩展点,通过onRemoval进入删除之后的业务逻辑,默认是空实现。
public final void remove(InternalThreadLocalMap threadLocalMap) {
// ..
Object v = threadLocalMap.removeIndexedVariable(index);
removeFromVariablesToRemove(threadLocalMap, this);
if (v != InternalThreadLocalMap.UNSET) {
try {
onRemoval((V) v);
}
}
}
四、总结
FastThreadLocal实际上采用的是数组的方式进行存储数据,在数据的获取、赋值都是通过下标的方式进行,而ThreadLocal是通过map结构,先计算哈希值,在进行线性探测的方式进行定位。所以在高并发下,FastThreadLocal应该相对高效,但是FastThread有一个弊端就是index是一直累加,也就是说如果移除了某个变量是通过将对应下标的元素标记为UNSET占位,而不进行回收,会无限制增大,会触发扩容等一些问题。