前言
Netty很多地方使用了自己实现的ThreadLocal---FastThreadLocal,本章学习一下FastThreadLocal相对于传统JDK的ThreadLocal的优势。
一、ThreadLocal
ThreadLocal的问题是什么?为什么要重新实现?
1、数据结构
ThreadLocal实际使用ThreadLocalMap存储ThreadLocal实例与用户变量的映射关系。
public void set(T value) {
// 获取当前线程里的threadLocals成员变量,即ThreadLocalMap
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
// 传入当前ThreadLocal实例和用户变量value
map.set(this, value);
}
而ThreadLocalMap内部使用哈希表存储ThreadLocal实例与用户变量的映射关系,用哈希表不可避免地会遇到哈希冲突,而产生额外的插入和查询的开销。
解决哈希冲突的四种办法:
- 开放地址法:通过index=hash(key)得到下标,如果发生冲突,使用index=fun(index)计算得到下一个下标,用fun函数迭代n次,直到没有冲突。
- 再哈希法:通过index=hash1(key)得到下标,如果发生冲突,使用index=hash2(key)计算得到下一个下标,使用n个哈希函数,直到没有冲突。
- 拉链法:哈希表的entry为链表头节点,通过index=hash(key)得到下标,如果发生冲突,从index位置的链表头节点开始向后寻找。
- 公共溢出区:将哈希表分为基本表和溢出表两部分,凡是和基本表发生冲突的元素,一律填入溢出表。
ThreadLocalMap使用开放地址法解决哈希冲突,当发生哈希冲突时尝试放入下一个索引位置。
private void set(ThreadLocal<?> key, Object value) {
// 哈希表
Entry[] tab = table;
int len = tab.length;
// 用ThreadLocal实例的hashCode & 哈希表长度-1 得到哈希表下标
int i = key.threadLocalHashCode & (len-1);
// 开放地址法解决哈希冲突
// nextIndex取i=i+1,另外通过len控制,如果溢出则归零
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
ThreadLocal<?> k = e.get();
// 找到ThreadLocal=当前待插入的ThreadLocal,结束
if (k == key) {
e.value = value;
return;
}
// ThreadLocal作为Entry的key,是虚引用,如果没有强引用,发生GC时会被回收
if (k == null) {
// 替换过期entry(还清理了过期entry)
replaceStaleEntry(key, value, i);
return;
}
}
// 循环了一圈,没找到ThreadLocal对应在哈希表里的entry
// 但是找到了空闲的下标,放进去
tab[i] = new Entry(key, value);
int sz = ++size;
// 扩容需要rehash
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
可以看到,简单的一个set操作,ThreadLocalMap做了一堆事情。包括解决哈希冲突,扩容rehash,处理ThreadLocal做为虚引用Entry的key被回收的情况。
2、边界条件
ThreadLocal的另外一个问题,就是为了程序的健壮性,需要做一些边界条件的控制。最显而易见的是,哈希表entry的key是虚引用ThreadLocal。
为什么个key要用虚引用呢?从javadoc里找到了答案。
To help deal with very large and long-lived usages, the hash table entries use WeakReferences for keys. However, since reference queues are not used, stale entries are guaranteed to be removed only when the table starts running out of space.
为了处理非常长的使用寿命,哈希表entry使用WeakReferences作为键。 但是,由于不使用参考队列,因此仅在表空间不足时,才保证删除过时的entry。
问题在于,何时算作表空间不足,会执行过时entry删除?
在前面的set方法中,replaceStaleEntry和cleanSomeSlots方法调用,都涉及到了清理过期entry的逻辑,这里不再赘述。看一下get方法。
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
// 从哈希表中查找ThreadLocal对应的Entry
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
return (T)e.value;
}
}
return setInitialValue();
}
private Entry getEntry(ThreadLocal<?> key) {
int i = key.threadLocalHashCode & (table.length - 1);
Entry e = table[i];
if (e != null && e.get() == key)
return e;
else
// 如果e == null || 发生哈希冲突e.get() != key
return getEntryAfterMiss(key, i, e);
}
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
Entry[] tab = table;
int len = tab.length;
// 采用开放地址法,搜索哈希表
while (e != null) {
ThreadLocal<?> k = e.get();
if (k == key)
// 找到entry
return e;
if (k == null)
// 发现虚引用ThreadLocal被回收,执行过期entry回收逻辑
expungeStaleEntry(i);
else
// i = i + 1
i = nextIndex(i, len);
e = tab[i];
}
return null;
}
可以看到,如果通过ThreadLocal的threadLocalHashCode&len-1从哈希表中获取元素失败,会进入getEntryAfterMiss方法。在搜索过程中,如果发现Entry的key为null,表示ThreadLocal实例在GC时被回收,则需要执行过期entry回收逻辑expungeStaleEntry。
3、Netty对于ThreadLocal的改进
针对上面的两个问题,Netty做了如下改进:
- 数据结构采用纯数组代替哈希表,每个ThreadLocal实例化时分配唯一ID,作为ThreadLocalMap里数组的下标。由于没有使用哈希表,不存在哈希冲突,而且底层容器扩容不涉及rehash。
- 在ThreadLocalMap中,没有Entry概念,且ThreadLocal不会作为虚引用放在ThreadLocalMap的数组中。没有虚引用,不需要考虑清除过期entry。
二、FastThreadLocal
1、FastThreadLocalThread
为了改进ThreadLocal,FastThreadLocalThread通过继承Thread,扩展了一个成员变量InternalThreadLocalMap,用于存放当前线程的线程变量,代替JDK的ThreadLocalMap。
public class FastThreadLocalThread extends Thread {
private InternalThreadLocalMap threadLocalMap;
public final InternalThreadLocalMap threadLocalMap() {
return threadLocalMap;
}
public final void setThreadLocalMap(InternalThreadLocalMap threadLocalMap) {
this.threadLocalMap = threadLocalMap;
}
}
2、UnpaddedInternalThreadLocalMap
UnpaddedInternalThreadLocalMap是InternalThreadLocalMap的父类。
class UnpaddedInternalThreadLocalMap {
// 兜底使用老ThreadLocal
static final ThreadLocal<InternalThreadLocalMap> slowThreadLocalMap = new ThreadLocal<InternalThreadLocalMap>();
// 计数器 用于给FastThreadLocal分发ID(数组下标)
static final AtomicInteger nextIndex = new AtomicInteger();
// 代替ThreadLocalMap里的哈希表 这里使用纯数组
Object[] indexedVariables;
// 其他Netty自己使用的线程变量,没有放在indexedVariables中
int futureListenerStackDepth;
int localChannelReaderStackDepth;
Map<Class<?>, Boolean> handlerSharableCache;
IntegerHolder counterHashCode;
ThreadLocalRandom random;
Map<Class<?>, TypeParameterMatcher> typeParameterMatcherGetCache;
Map<Class<?>, Map<String, TypeParameterMatcher>> typeParameterMatcherFindCache;
StringBuilder stringBuilder;
Map<Charset, CharsetEncoder> charsetEncoderCache;
Map<Charset, CharsetDecoder> charsetDecoderCache;
ArrayList<Object> arrayList;
// 构造函数 传入存放线程变量的数组
UnpaddedInternalThreadLocalMap(Object[] indexedVariables) {
this.indexedVariables = indexedVariables;
}
}
UnpaddedInternalThreadLocalMap持有几个重要的成员变量:
- slowThreadLocalMap:为了防止使用FastThreadLocal的客户端,无法避免地使用了普通的Thread(没有用FastThreadLocalThread),降级使用ThreadLocal存储InternalThreadLocalMap实例,具备ThreadLocal同样的能力。
- nextIndex:一个原子计数器,用于给FastThreadLocal实例生成ID,作为InternalThreadLocalMap里的数组(indexedVariables)下标。
- indexedVariables:下标由nextIndex计数器生成由FastThreadLocal持有,元素是客户端需要存放的元素。
- 其他:其他变量都是Netty框架自己需要使用的变量。为什么这些变量不一起存放在indexedVariables中?考虑每次框架需要新增一个线程变量,都会占用indexedVariables中的一个容量,导致:1)indexedVariables初始化容量需要每次调整 2)如果不调整数组初始化容量,容易引发运行时的数组自动扩容
3、InternalThreadLocalMap
成员变量
public final class InternalThreadLocalMap extends UnpaddedInternalThreadLocalMap {
// 数组初始化大小
private static final int INDEXED_VARIABLE_TABLE_INITIAL_SIZE = 32;
// 一个常量,用于标记数组中空闲位置
public static final Object UNSET = new Object();
}
构造方法
private InternalThreadLocalMap() {
super(newIndexedVariableTable());
}
private static Object[] newIndexedVariableTable() {
Object[] array = new Object[INDEXED_VARIABLE_TABLE_INITIAL_SIZE];
Arrays.fill(array, UNSET);
return array;
}
构建方法是私有的,InternalThreadLocalMap在被外部获取时(get方法)懒加载。创建了32长度Object数组,并用UNSET对象填充,传给父类UnpaddedInternalThreadLocalMap构造方法。
获取InternalThreadLocalMap
FastThreadLocal和ThreadLocal一样,需要获取当前线程对应的ThreadLocalMap,所以这里要提供一个静态方法给FastThreadLocal拿到这个存储元素的容器InternalThreadLocalMap。
public static InternalThreadLocalMap get() {
Thread thread = Thread.currentThread();
if (thread instanceof FastThreadLocalThread) {
return fastGet((FastThreadLocalThread) thread);
} else {
return slowGet();
}
}
可以看到这个做了个兼容的操作,如果thread是FastThreadLocalThread,走fastGet方法;否则走slowGet方法。
fastGet方法如下,先获取FastThreadLocalThread里的InternalThreadLocalMap,如果为空,则创建并放入线程的成员变量中。
private static InternalThreadLocalMap fastGet(FastThreadLocalThread thread) {
InternalThreadLocalMap threadLocalMap = thread.threadLocalMap();
if (threadLocalMap == null) {
thread.setThreadLocalMap(threadLocalMap = new InternalThreadLocalMap());
}
return threadLocalMap;
}
slowGet方法如下,借助UnpaddedInternalThreadLocalMap的ThreadLocal成员变量,保存InternalThreadLocalMap实例,实现普通Thread也可以使用FastThreadLocal,做了一个兼容操作。
private static InternalThreadLocalMap slowGet() {
ThreadLocal<InternalThreadLocalMap> slowThreadLocalMap = UnpaddedInternalThreadLocalMap.slowThreadLocalMap;
InternalThreadLocalMap ret = slowThreadLocalMap.get();
if (ret == null) {
ret = new InternalThreadLocalMap();
slowThreadLocalMap.set(ret);
}
return ret;
}
存储元素
public boolean setIndexedVariable(int index, Object value) {
Object[] lookup = indexedVariables;
if (index < lookup.length) {
// 如果index没超出数组容量,直接设置并返回
Object oldValue = lookup[index];
lookup[index] = value;
return oldValue == UNSET;
} else {
// 如果index超出数组容量,执行扩容并设置
expandIndexedVariableTableAndSet(index, value);
return true;
}
}
扩容并保存元素。注意扩容逻辑,取的是大于index的最接近的2的n次幂,如index=32,扩容大小为64,index=64,扩容大小为128,index=65,扩容大小也为128。
private void expandIndexedVariableTableAndSet(int index, Object value) {
Object[] oldArray = indexedVariables;
final int oldCapacity = oldArray.length;
// 计算扩容大小
int newCapacity = index;
newCapacity |= newCapacity >>> 1;
newCapacity |= newCapacity >>> 2;
newCapacity |= newCapacity >>> 4;
newCapacity |= newCapacity >>> 8;
newCapacity |= newCapacity >>> 16;
newCapacity ++;
// 数组拷贝
Object[] newArray = Arrays.copyOf(oldArray, newCapacity);
// UNSET标志填充
Arrays.fill(newArray, oldCapacity, newArray.length, UNSET);
// 保存元素
newArray[index] = value;
indexedVariables = newArray;
}
查询元素
public Object indexedVariable(int index) {
Object[] lookup = indexedVariables;
return index < lookup.length? lookup[index] : UNSET;
}
4、FastThreadLocal
成员变量
public class FastThreadLocal<V> {
// 类变量,这个下标位置用于存储所有FastThreadLocal实例集合
private static final int variablesToRemoveIndex = InternalThreadLocalMap.nextVariableIndex();
// 每个FastThreadLocal创建时会分配一个InternalThreadLocalMap控制的唯一id
// 作为 UnpaddedInternalThreadLocalMap.indexedVariables的数组下标
private final int index;
}
构造方法
FastThreadLocal构造时通过UnpaddedInternalThreadLocalMap#nextIndex原子计数器,分配了InternalThreadLocalMap的Object数组的下标位置,用于后续get/set。
public FastThreadLocal() {
index = InternalThreadLocalMap.nextVariableIndex();
}
set
简单来看,set方法只是将value存储到Object数组分配的index位置。
public final void set(V value) {
if (value != InternalThreadLocalMap.UNSET) {
// 获取当前线程对应的InternalThreadLocalMap
InternalThreadLocalMap threadLocalMap = InternalThreadLocalMap.get();
// 存储value
setKnownNotUnset(threadLocalMap, value);
} else {
// 如果value是UNSET对象,移除当前threadLocalMap
remove();
}
}
private void setKnownNotUnset(InternalThreadLocalMap threadLocalMap, V value) {
// 如果保存成功
if (threadLocalMap.setIndexedVariable(index, value)) {
// 把当前存储FastThreadLocal到variablesToRemoveIndex下标对应的FastThreadLocal集合中
// 为的是后期可以提供静态方法,批量清理当前线程关联的FastThreadLocal
// 忽略实现
addToVariablesToRemove(threadLocalMap, this);
}
}
get
get方法先尝试从Object数组的index位置获取对象,如果对象不为UNSET则返回,否则执行initialize方法,设置一个初始值。initialize方法需要客户端实现,和ThreadLocal一样。
public final V get(InternalThreadLocalMap threadLocalMap) {
Object v = threadLocalMap.indexedVariable(index);
if (v != InternalThreadLocalMap.UNSET) {
return (V) v;
}
return initialize(threadLocalMap);
}
三、简单实现
如果不考虑任何边界条件和Thread兼容,简单来说FastThreadLocal的实现如下:
public class DemoFastThread extends Thread {
private DemoThreadLocalMap threadLocalMap;
public DemoFastThread(Runnable target) {
super(target);
this.threadLocalMap = new DemoThreadLocalMap();
}
public DemoThreadLocalMap getThreadLocalMap() {
return threadLocalMap;
}
public void setThreadLocalMap(DemoThreadLocalMap threadLocalMap) {
this.threadLocalMap = threadLocalMap;
}
}
class DemoThreadLocalMap {
Object[] objs = new Object[32];
static final AtomicInteger nextIndex = new AtomicInteger();
public Object getObj(int index) {
return objs[index];
}
public void setObj(int index, Object obj) {
this.objs[index] = obj;
}
}
class DemoFastThreadLocal<V> {
private int index;
public DemoFastThreadLocal() {
this.index = DemoThreadLocalMap.nextIndex.getAndIncrement();
}
public final V get() {
DemoThreadLocalMap threadLocalMap = ((DemoFastThread) Thread.currentThread()).getThreadLocalMap();
Object v = threadLocalMap.getObj(index);
if (v == null) {
v = initialValue();
threadLocalMap.setObj(index, v);
}
return (V) v;
}
public final void set(V v) {
DemoThreadLocalMap threadLocalMap = ((DemoFastThread) Thread.currentThread()).getThreadLocalMap();
threadLocalMap.setObj(index, v);
}
protected V initialValue() {
return null;
}
}