前面在分析NioEventLoop源码时提到过,Netty默认使用io.netty.util.concurrent.DefaultThreadFactory线程工厂来创建新线程,它会创建FastThreadLocalThread线程来驱动NioEventLoop的执行,而不是JDK原生的Thread,原因是FastThreadLocalThread可以提升FastThreadLocal的性能。
既然JDK已经提供了ThreadLocal,为何Netty还要重复造轮子呢?原因无它,就是因为JDK的ThreadLocal效率不是很高。
JDK将ThreadLocal作为Key,值作为Value存放到ThreadLocalMap中,每个Thread都维护了一个Map容器。当线程使用的ThreadLocal对象逐渐增多,出现哈希冲突的概率就会变大,ThreadLocalMap处理哈希冲突的方式是「线性探测」,即根据Key的哈希计算index,如果数组的该下标已经被占用,代表出现哈希冲突,它会环形的寻找下一个槽位,直到找到一个空的槽位,再将映射关系封装成Entry节点保存到数组。get()操作也是一样的流程,遇到哈希冲突,就要环形寻找。
总结就是JDK的ThreadLocal,一旦出现哈希冲突,读写的时间复杂度会从O(1)变成O(n),Netty为了更高的性能,自己实现了一个更快的FastThreadLocal,本篇文章就带你揭秘FastThreadLocal的高性能内幕。
FastThreadLocal源码
FastThreadLocal是ThreadLocal的一个变体,当它和FastThreadLocalThread一起使用时,能提供更好的访问性能。
需要注意,必须配合FastThreadLocalThread使用,否则会退化成JDK的ThreadLocal,效率可能反而会有影响。
InternalThreadLocalMap
InternalThreadLocalMap是Netty用来代替JDK中的ThreadLocal.ThreadLocalMap类的,InternalThreadLocalMap使用数组来代替Hash表,每个FastThreadLocal被创建时,会拥有一个全局唯一且递增的索引index,该index就代表FastThreadLocal对应数组的下标,Value会被直接放到该下标处,访问也是一样,根据index快速定位元素,非常的快速,压根就不存在哈希冲突,时间复杂度始终是O(1),缺点就是会浪费点内存空间,不过在内存越来越廉价的今天,这是值得的。
先看几个和FastThreadLocal相关的属性,后面会用到:
/*
非FastThreadLocalThread线程使用FastThreadLocal,Netty会创建一个InternalThreadLocalMap,
保存到原生Thread.threadLocals里,相当于往原生ThreadLocalMap里又放了一个Map。
*/
private static final ThreadLocal<InternalThreadLocalMap> slowThreadLocalMap =
new ThreadLocal<InternalThreadLocalMap>();
// index生成器
private static final AtomicInteger nextIndex = new AtomicInteger();
// 默认的数组大小
private static final int INDEXED_VARIABLE_TABLE_INITIAL_SIZE = 32;
// 代替Null的一个对象,代表槽位未设值
public static final Object UNSET = new Object();
// 存放FastThreadLocal对应的Value
private Object[] indexedVariables;
再看FastThreadLocal的属性,静态常量variablesToRemoveIndex的值是0,它会在数组的0号位存储一个Set<FastThreadLocal>来保存线程使用过的FastThreadLocal,目的是为了在removeAll()方法中进行批量的移除。
实例常量index代表FastThreadLocal的唯一索引,它是全局唯一且递增的。
/*
占据的是数组0号下标,存放的是Set<FastThreadLocal>:当前线程使用到的所有FastThreadLocal。
目的是FastThreadLocal.removeAll()时批量移除。
*/
private static final int variablesToRemoveIndex = InternalThreadLocalMap.nextVariableIndex();
// FastThreadLocal的唯一索引,全局唯一且递增,从1开始,0存放需要移除的FastThreadLocal。
private final int index;
FastThreadLocal的构造函数非常简单,就是生成一个索引。
public FastThreadLocal() {
// index初始化,通过一个全局的AtomicInteger递增
index = InternalThreadLocalMap.nextVariableIndex();
}
set()源码
调用set()方法保存值时,它会判断Value是否是UNSET,如果是UNSET代表移除该实例,否则才是设置新值。
首先需要获取到线程绑定的InternalThreadLocalMap,然后根据index将InternalThreadLocalMap内的indexedVariables数组对应的下标填充Value。
public final void set(V value) {
if (value != InternalThreadLocalMap.UNSET) {
// 获取当前线程绑定的InternalThreadLocalMap
InternalThreadLocalMap threadLocalMap = InternalThreadLocalMap.get();
setKnownNotUnset(threadLocalMap, value);
} else {
// 值为UNSET则代表是移除操作
remove();
}
}
InternalThreadLocalMap.get()用来获取当前线程的InternalThreadLocalMap对象,如果线程是FastThreadLocalThread,则可以快速获取,因为FastThreadLocalThread使用一个属性来记录它了。如果线程是普通的Thread,则会慢速获取,利用JDK原生的ThreadLocal来保存InternalThreadLocalMap。
// 获取当前线程的InternalThreadLocalMap
public static InternalThreadLocalMap get() {
Thread thread = Thread.currentThread();
if (thread instanceof FastThreadLocalThread) {
// 如果当前线程是FastThreadLocalThread,则直接取变量threadLocalMap即可。
return fastGet((FastThreadLocalThread) thread);
} else {
// 非FastThreadLocalThread线程,Netty也做了兼容,只是性能会有所影响。
return slowGet();
}
}
对于非FastThreadLocalThread线程,Netty也做了兼容,退化成JDK的ThreadLocal,在ThreadLocal中保存InternalThreadLocalMap对象。
/*
非FastThreadLocalThread线程也可以使用FastThreadLocal,Netty做了兼容,只是性能会有所影响。
*/
private static InternalThreadLocalMap slowGet() {
/*
非FastThreadLocalThread线程使用FastThreadLocal,Netty会创建一个InternalThreadLocalMap,
保存到原生Thread.threadLocals里,相当于往原生ThreadLocalMap里又放了一个Map。
这里就是从原生ThreadLocal中取出InternalThreadLocalMap,如果没有则塞一个进去。
*/
InternalThreadLocalMap ret = slowThreadLocalMap.get();
if (ret == null) {
ret = new InternalThreadLocalMap();
slowThreadLocalMap.set(ret);
}
return ret;
}
获取到了线程绑定的InternalThreadLocalMap,接下来就是将Value设置到数组了,会调用setKnownNotUnset()方法:
/*
Set值,已知不是UNSET,在set()已经判断过了
*/
private void setKnownNotUnset(InternalThreadLocalMap threadLocalMap, V value) {
// 将Object[]的index下标设为value
if (threadLocalMap.setIndexedVariable(index, value)) {
// 将当前FastThreadLocal添加到0号Set里,方便后面的removeAll()时使用。
addToVariablesToRemove(threadLocalMap, this);
}
}
threadLocalMap.setIndexedVariable()会将Value设置到数组的指定下标,如果需要扩容则会进行扩容。
// 将value设置到数组的index下标位置
public boolean setIndexedVariable(int index, Object value) {
Object[] lookup = indexedVariables;
if (index < lookup.length) {
Object oldValue = lookup[index];
lookup[index] = value;
return oldValue == UNSET;
} else {
// 扩容并且Set
expandIndexedVariableTableAndSet(index, value);
return true;
}
}
Value保存到数组后,需要将FastThreadLocal添加到数组0号位的Set容器中,因为removeAll()时需要批量移除所有的FastThreadLocal。
// 将FastThreadLocal保存到数组0号位的Set容器中,等待后面的批量删除
private static void addToVariablesToRemove(InternalThreadLocalMap threadLocalMap, FastThreadLocal<?> variable) {
// 取出0号固定位元素
Object v = threadLocalMap.indexedVariable(variablesToRemoveIndex);
Set<FastThreadLocal<?>> variablesToRemove;
if (v == InternalThreadLocalMap.UNSET || v == null) {
// 没有值,则设置为Set<FastThreadLocal>
variablesToRemove = Collections.newSetFromMap(new IdentityHashMap<FastThreadLocal<?>, Boolean>());
threadLocalMap.setIndexedVariable(variablesToRemoveIndex, variablesToRemove);
} else {
// 有值就强转为Set
variablesToRemove = (Set<FastThreadLocal<?>>) v;
}
// 将FastThreadLocal添加到Set中,removeAll()时需要用到。
variablesToRemove.add(variable);
}
set()操作到此就结束了。
get()源码
清楚了set()的流程,再看get()就已经非常简单了。
要想获取当前当前线程对应FastThreadLocal的Value,首先肯定还是需要获取到线程绑定的InternalThreadLocalMap,然后根据index去数组中取值,如果取到了就直接返回,没取到则根据initialize()填充初始值。
public final V get() {
/*
获取当前线程绑定的InternalThreadLocalMap
1.对于FastThreadLocalThread,它直接使用属性threadLocalMap保存。
2.对于非FastThreadLocalThread线程,会创建一个并塞到原生ThreadLocal中。
*/
InternalThreadLocalMap threadLocalMap = InternalThreadLocalMap.get();
/*
全局的FastThreadLocal对应的Value都放在InternalThreadLocalMap的Object[]里,根据index下标即可快速访问。
*/
Object v = threadLocalMap.indexedVariable(index);
if (v != InternalThreadLocalMap.UNSET) {// UNSET可以理解为null的替代品,代表 无值,没有设置值的一个默认对象。
return (V) v;
}
// 没有值,则初试化
return initialize(threadLocalMap);
}
threadLocalMap.indexedVariable()非常简单,就是根据index定位数组元素:
public Object indexedVariable(int index) {
Object[] lookup = indexedVariables;
return index < lookup.length? lookup[index] : UNSET;
}
如果元素为UNSET,说明还没有设置过值,需要进行初始化。
/*
get()发现值没有设置,会调用该方法进行初始化
*/
private V initialize(InternalThreadLocalMap threadLocalMap) {
V v = null;
try {
v = initialValue();// 默认返回null,子类重写
} catch (Exception e) {
PlatformDependent.throwException(e);
}
// 将初始化的值保存到数组中
threadLocalMap.setIndexedVariable(index, v);
// 将FastThreadLocal添加到数组0号位的Set容器中
addToVariablesToRemove(threadLocalMap, this);
return v;
}
至此,get()流程也全部结束。
remove()源码
FastThreadLocal使用完毕记得即使移除掉,调用remove()方法即可。
要想移除FastThreadLocal,首先依然是要获取到线程绑定的InternalThreadLocalMap。
public final void remove() {
// 获取当前线程绑定的InternalThreadLocalMap,再remove
remove(InternalThreadLocalMap.getIfSet());
}
接下来需要做三件事:
- 根据FastThreadLocal的index将数组中指定位置填充UNSET。
- 从Set容器中删除FastThreadLocal对象。
- 需要触发
onRemoval()钩子函数。
// 从InternalThreadLocalMap中移除当前FastThreadLocal
public final void remove(InternalThreadLocalMap threadLocalMap) {
if (threadLocalMap == null) {
return;
}
// 从数组中删除对应下标的值:重置为UNSET
Object v = threadLocalMap.removeIndexedVariable(index);
// 从数组0号位的Set容器中删除
removeFromVariablesToRemove(threadLocalMap, this);
if (v != InternalThreadLocalMap.UNSET) {
try {
// 触发钩子函数
onRemoval((V) v);
} catch (Exception e) {
PlatformDependent.throwException(e);
}
}
}
1.根据FastThreadLocal的index将数组中指定位置填充UNSET。
public Object removeIndexedVariable(int index) {
Object[] lookup = indexedVariables;
if (index < lookup.length) {
Object v = lookup[index];
// 填充UNSET,代表删除
lookup[index] = UNSET;
return v;
} else {
return UNSET;
}
}
2.取出数组0号位的Set容器,删除FastThreadLocal对象。
// 从0号固定位中取出Set<FastThreadLocal>并移除指定FastThreadLocal
private static void removeFromVariablesToRemove(
InternalThreadLocalMap threadLocalMap, FastThreadLocal<?> variable) {
// 取出数组0号位的Set容器
Object v = threadLocalMap.indexedVariable(variablesToRemoveIndex);
if (v == InternalThreadLocalMap.UNSET || v == null) {
return;
}
@SuppressWarnings("unchecked")
Set<FastThreadLocal<?>> variablesToRemove = (Set<FastThreadLocal<?>>) v;
// 从容器中删除FastThreadLocal
variablesToRemove.remove(variable);
}
3.如果要移除的Value不是UNSET,说明曾经设置过具体的值,再移除它时需要触发onRemoval()钩子函数,让子类能够监听到这个移除动作。默认什么也不做,子类重写。
// value被移除时触发的回调,默认什么也不做,子类实现
protected void onRemoval(@SuppressWarnings("UnusedParameters") V value) throws Exception {
}
removeAll()源码
removeAll()是静态方法,它不针对某个FastThreadLocal,而是将当前线程的所有FastThreadLocal全部移除。
首先仍然需要获取到当前线程绑定的InternalThreadLocalMap,从数组的0号位取出Set容器,遍历Set容器,按个移除FastThreadLocal。
/*
移除所有绑定到当前线程的FastThreadLocal实例。
*/
public static void removeAll() {
// 获取当前绑定的InternalThreadLocalMap
InternalThreadLocalMap threadLocalMap = InternalThreadLocalMap.getIfSet();
if (threadLocalMap == null) {
return;
}
try {
// 取出0号固定位元素 就是Set<FastThreadLocal>,迭代遍历,依次remove()
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]);
for (FastThreadLocal<?> tlv: variablesToRemoveArray) {
// 遍历 按个删除
tlv.remove(threadLocalMap);
}
}
} finally {
// 最后将InternalThreadLocalMap重置
InternalThreadLocalMap.remove();
}
}
总结
Netty的FastThreadLocal是JDK ThreadLocal的一个变体,当它和FastThreadLocalThread一起使用时,能提供更好的访问性能。
它优化的思路是使用数组来代替JDK的哈希表,避免了哈希冲突,使得读写的时间复杂度始终能保持在O(1)。缺点就是会浪费一定的内存空间,当FastThreadLocal数量过大时,全局递增的索引就会很大,数组的长度也会越来越长,而且索引只会递增不会递减,这意味这数组只会扩容而不会缩容,开发者需要特别注意FastThreadLocal的对象数量,不要滥用,否则会因为无法申请一大块连续的内存空间引起频繁GC,最终导致OOM!