版本信息:
JDK1.8
Netty-all:4.1.38.Final
传统的ThreadLocal机制
讲netty的FastThreadLocal机制,就不得不提及到JDK自带的ThreadLocal机制,所以下面会用一小段篇幅介绍一下ThreadLocal机制~
ThreadLocal的机制,大致的解释为线程本地变量,独属于线程,所以每个线程有一份,所以互相独立、隔离、线程安全、线程中任意地方可存可取。在Java中Thread类中存在一个集合用于实现此功能。
public class Thread implements Runnable {
…………
ThreadLocal.ThreadLocalMap threadLocals = null;
…………
}
所以我们需要大致看一下如何使用,获取元素的源码如下:
public T get() {
// 获取到线程本地集合。
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
// 根据ThreadLocal的Hash值得到集合的元素
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
// 如果没有线程本地集合,那就创建一个。
return setInitialValue();
}
private Entry getEntry(ThreadLocal<?> key) {
// 根据Hash值来定位索引
int i = key.threadLocalHashCode & (table.length - 1);
Entry e = table[i];
if (e != null && e.get() == key)
// 直接命中,就返回
return e;
else
// 如果冲突,往后面找
return getEntryAfterMiss(key, i, e);
}
我们清楚,Hash运算虽然是O1时间复杂度,但是这只是最理想状态,因为无法避免Hash碰撞,当元素多起来,甚至能达到On的时间复杂度。而JDK原生的ThreadLocal就是通过Hash来计算索引下标,当发生Hash碰撞往后找临近节点插入,并不能达到理想的O1时间复杂度。
Netty中FastThreadLocal机制
上文介绍了JDK自带的ThreadLocal机制,他是根据Hash算法来存放节点,因为Hash碰撞的原因所以达不到O1的时间复杂度,而网传 " Netty的FastThreadLocal机制比它快3倍 " 所以下文介绍FastThreadLocal到底快在那里,如何实现的。
在这之前还需要介绍一个类 FastThreadLocalThread ,从上文我们知道JDK自带的ThreadLocal机制是通过Thread对象中ThreadLocal.ThreadLocalMap 集合存放数据,而 Netty的FastThreadLocal肯定也是需要有一个容器来存放数据,所以Netty设计了FastThreadLocalThread类,它实现与原生的Thread类,在其中定义了InternalThreadLocalMap集合。
public class FastThreadLocalThread extends Thread {
…………
private InternalThreadLocalMap threadLocalMap;
…………
}
不妨看一下 InternalThreadLocalMap 类的定义,可以看到内部比较简单,实现UnpaddedInternalThreadLocalMap类,在UnpaddedInternalThreadLocalMap类中定义了一个数组用于存放FastThreadLocal的对象,还有兼容原生线程的ThreadLocal,以及一个原子类用于产出索引值。
public final class InternalThreadLocalMap extends UnpaddedInternalThreadLocalMap {
}
class UnpaddedInternalThreadLocalMap {
// 用于兼容,当线程非FastThreadLocalThread的情况下使用。
static final ThreadLocal<InternalThreadLocalMap> slowThreadLocalMap = new ThreadLocal<InternalThreadLocalMap>();
// 用于原子性的产出索引值。
// 注意这个变量是被static final修饰的
static final AtomicInteger nextIndex = new AtomicInteger();
// 存放FastThreadLocal管理的对象。
Object[] indexedVariables;
}
铺垫做好了,接下来就看到FastThreadLocal类实现。
public class FastThreadLocal<V> {
// 注意这里是static final修饰的,所以在clinit的时候初始化
// InternalThreadLocalMap.nextVariableIndex() 这个方法是得到索引值
// 所以这个值恒定为0,用作与特殊用途
private static final int variablesToRemoveIndex = InternalThreadLocalMap.nextVariableIndex();
// 索引值
private final int index;
public FastThreadLocal() {
// 在构造方法中算出当前FastThreadLocal所属的索引值,而这个索引值用于InternalThreadLocalMap中。
index = InternalThreadLocalMap.nextVariableIndex();
}
}
public static int nextVariableIndex() {
// 因为nextIndex是static修饰的,所以这里全局唯一。
// 原子性自增,得到索引值。
int index = nextIndex.getAndIncrement();
if (index < 0) {
nextIndex.decrementAndGet();
throw new IllegalStateException("too many thread-local indexed variables");
}
return index;
}
在FastThreadLocal类中有一个索引值,此索引值在构造方法中初始化,目的是原子性的获取到当前FastThreadLocal的索引,而这个索引用途在于InternalThreadLocalMap 直接O1的时间复杂度找到元素。而因为每创建一个FastThreadLocal,UnpaddedInternalThreadLocalMap类中被static修饰的全局原子类索引值就会+1,所以就避免了重复的可能性。
接下来,就看到如何使用FastThreadLocal,当然是通过get方法获取,获取不到就创建,然后放入集合中,下次就能够命中。这个跟ThreadLocal一模一样的思想。
public class FastThreadLocal<V> {
public final V get() {
// 取出线程中InternalThreadLocalMap集合
// 当然必须是FastThreadLocalThread线程,如果是原生Thread对象的话,会使用原生ThreadLocal对象来兼容
InternalThreadLocalMap threadLocalMap = InternalThreadLocalMap.get();
Object v = threadLocalMap.indexedVariable(index);
// 命中就直接返回
if (v != InternalThreadLocalMap.UNSET) {
return (V) v;
}
// 没命中就创建
return initialize(threadLocalMap);
}
private V initialize(InternalThreadLocalMap threadLocalMap) {
V v = null;
try {
// 模板方法,给子类实现。
v = initialValue();
} catch (Exception e) {
PlatformDependent.throwException(e);
}
// 添加到线程对象的InternalThreadLocalMap集合中,下次就能够直接命中了。
threadLocalMap.setIndexedVariable(index, v);
// 把当前FastThreadLocal添加到移除队列中,当执行完毕后,Netty自动帮你回收空间。
addToVariablesToRemove(threadLocalMap, this);
return v;
}
private static void addToVariablesToRemove(InternalThreadLocalMap threadLocalMap, FastThreadLocal<?> variable) {
// 拿到variablesToRemoveIndex索引的值,而variablesToRemoveIndex恒定为0。
// 也即0号特殊用途。
Object v = threadLocalMap.indexedVariable(variablesToRemoveIndex);
Set<FastThreadLocal<?>> variablesToRemove;
// 第一次是UNSET,所以去初始化
if (v == InternalThreadLocalMap.UNSET || v == null) {
// 创建一个集合,用于存放已经使用的FastThreadLocal,后续用于自动资源回收
variablesToRemove = Collections.newSetFromMap(new IdentityHashMap<FastThreadLocal<?>, Boolean>());
threadLocalMap.setIndexedVariable(variablesToRemoveIndex, variablesToRemove);
} else {
// 后续只需要取出来即可。
variablesToRemove = (Set<FastThreadLocal<?>>) v;
}
// 把当前FastThreadLocal添加到集合中。
variablesToRemove.add(variable);
}
}
对以上FastThreadLocal的get方法做一个总结:
- 获取到FastThreadLocalThread线程的InternalThreadLocalMap集合,如果是原生的Thread线程就会使用ThreadLocal兼容InternalThreadLocalMap集合(所以,如果使用FastThreadLocal,不使用FastThreadLocalThread线程,那么效率会比原生的还低)
- 通过FastThreadLocal的索引从InternalThreadLocalMap集合中获取到元素
- 如果命中就直接返回
- 如果不命中就调用initialize做创建和初始化工作
- 创建工作调用initialValue方法,此方法是一个模板方法,交给子类使用,所以具体的创建交给子类
- 创建完毕后,添加到InternalThreadLocalMap集合中,下次就可以直接命中
- 最后会调用addToVariablesToRemove方法,此方法会把InternalThreadLocalMap的0号索引放入一个Set<FastThreadLocal<?>> 集合,用于后续的自动回收资源。
所以上述的图需要做出修正,因为InternalThreadLocalMap的0号索引有特殊用途。用于自动回收资源,后续马上介绍。
对于自动回收资源,其实实现的原理非常简单,我们看到FastThreadLocalThread的构造方法。这里使用了最经典的装饰者模式,把原有的Runnable给做了增强,当线程执行完run方法,也即执行完业务逻辑后,在finally代码块中做资源回收,所以也称之为自动回收资源。
public class FastThreadLocalThread extends Thread {
public FastThreadLocalThread(Runnable target) {
super(FastThreadLocalRunnable.wrap(target));
cleanupFastThreadLocals = true;
}
}
final class FastThreadLocalRunnable implements Runnable {
private final Runnable runnable;
@Override
public void run() {
try {
runnable.run();
} finally {
// 资源回收
FastThreadLocal.removeAll();
}
}
static Runnable wrap(Runnable runnable) {
return runnable instanceof FastThreadLocalRunnable ? runnable : new FastThreadLocalRunnable(runnable);
}
}
最后再看一下,资源回收的代码逻辑。
public static void removeAll() {
InternalThreadLocalMap threadLocalMap = InternalThreadLocalMap.getIfSet();
try {
// 获取到0号索引的set集合
Object v = threadLocalMap.indexedVariable(variablesToRemoveIndex);
if (v != null && v != InternalThreadLocalMap.UNSET) {
@SuppressWarnings("unchecked")
Set<FastThreadLocal<?>> variablesToRemove = (Set<FastThreadLocal<?>>) v;
FastThreadLocal<?>[] variablesToRemoveArray =
variablesToRemove.toArray(new FastThreadLocal[0]);
// 遍历set集合中存放的所有的FastThraedLocal对象。
for (FastThreadLocal<?> tlv: variablesToRemoveArray) {
// 调用FastThraedLocal的remove方法。
tlv.remove(threadLocalMap);
}
}
} finally {
InternalThreadLocalMap.remove();
}
}
public final void remove(InternalThreadLocalMap threadLocalMap) {
// 从集合中删除当前节点
Object v = threadLocalMap.removeIndexedVariable(index);
removeFromVariablesToRemove(threadLocalMap, this);
if (v != InternalThreadLocalMap.UNSET) {
try {
// 模板方法,具体的回收资源细节,交给子类实现。
onRemoval((V) v);
} catch (Exception e) {
PlatformDependent.throwException(e);
}
}
}
这里也非常的简单,就是拿到InternalThreadLocalMap 中0号索引对应的set集合,此集合中存放着所有的FastThreadLocal,然后一一调用FastThreadLocal的remove方法,而remove方法中调用了onRemoval方法,此方法是个标准的模板方法,具体的回收细节交给子类实现,父类只帮你开启自动回收机制,回收细节还是暴露给开发者~
总结:
- FastThreadLocal完全是O1时间复杂度,不过是空间换时间罢了,存在一定的内存浪费(尤其是项目中大量使用FastThreadLocal,每个线程的InternalThreadLocalMap会越来越大,浪费会越来越多,不过,如今内存不值钱,用很小很小一部分内存换取执行效率是值得的~!)
- FastThreadLocal一定要配合FastThreadLocalThread线程使用,不然快速特性完全使用不到,并且会降至使用原生ThreadLocal机制,最终导致效率比原生ThreadLocal机制还慢(因为存在一定的包装)
- FastThreadLocal虽然有自动回收资源机制,但是的子类还是需要实现onRemoval方法,回收的细节交给开发者,比如一些堆外内存的释放等等~