前一篇文章简单分析了Netty中PooledByteBuf
相关结构,本文将围绕 FastThreadLocal
,来分析netty中当前线程分配内存的设计。
本文基于 4.1.38.Final 版本
FastThreadLocal
开始 进行PoolThreadLocalCache
之前,先需要介绍 FastThreadLocal
。这个很明显是在跟jdk的ThreadLocal
叫板,证明比他快。
先回一下jdk 的 ThreadLocal
相关原理吧:
ThreadLocal
估计大家都用过或者听过 Java 中 ThreadLocal
,即本地线程变量。以下情况可能用到:
- Mybatis 的 ErrorContext 中有使用
- Dubbo 的 RpcContext 也有使用
ThreadLocal
- 在日常spring mvc 开发中,可以将认证信息放入ThreadLocal中,存储当前调用者信息。
ThreadLocal原理
在Thread 类 中,有一个 ThreadLocalMap 变量:
ThreadLocal.ThreadLocalMap threadLocals = null;
从这里能猜出来,ThreadLocal果然是本地线程变量。
ThreadLocal 用法
正常情况下,使用 new ThreadLocal<T>()
即可使用ThreadLocal了
set方法:
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
- 从当前线程中,获取
threadLocals
变量 - 将
value
放入map中。
接下来主要看看 ThreadLocalMap
ThreadLocalMap
存储在本地线程变量中的为 ThreadLocalMap
,其内部是使用自定义Entry 来作为value维护这一个map。
static class ThreadLocalMap {
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
private Entry[] table;
ThreadLocalMap
中map是以Entry
作为key,且Entry
的ThreadLocal
类型的key为弱引用WeakReference
类型。- value为普通类型
弱引用类型特性为,如果没有强引用关联,那么当每一次gc,无论内存是否够用,都会回收。
回过头看看 ThreadLocalMap
的set 方法:
private void set(ThreadLocal<?> key, Object value) {
Entry[] tab = table;
int len = tab.length;
int i = key.threadLocalHashCode & (len-1);
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
ThreadLocal<?> k = e.get();
if (k == key) {
e.value = value;
return;
}
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}
tab[i] = new Entry(key, value);
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
- 寻址方式为
key.threadLocalHashCode & (len-1);
,即ThreadLocal
中threadLocalHashCode
值:
private final int threadLocalHashCode = nextHashCode();
private static int nextHashCode() {
return nextHashCode.getAndAdd(HASH_INCREMENT);
}
public final int getAndAdd(int delta) {
return unsafe.getAndAddInt(this, valueOffset, delta);
}
valueOffset 值则为 value 值内存中地址偏移量:
private static final long valueOffset;
static {
try {
valueOffset = unsafe.objectFieldOffset
(AtomicInteger.class.getDeclaredField("value"));
} catch (Exception ex) { throw new Error(ex); }
}
- 遍历index 后面 所有map,如果 寻址定位到的 e 不为空,做了下面几件事:
- 判断如果k等于当前ThreadLocal,则直接替换value 值,并退出
- 如果k 等于null,说明该位置的弱引用ThreadLocal被回收掉了,那么执行
replaceStaleEntry
对其进行清理,
replaceStaleEntry
里面其实做了挺多事,一旦发现有一个被弱引用回收的,肯定有可能不止一个:
replaceStaleEntry
部分代码:
for (int i = prevIndex(staleSlot, len);
(e = tab[i]) != null;
i = prevIndex(i, len))
if (e.get() == null)
slotToExpunge = i;
所以他会往前找到第一个不为null 的 entry且他的key被gc回收过。然后从这个节点开始,进一步调用 expungeStaleEntry
进行清理。
最后将这个entry 放入map中,即设置tab的 staleSlot 位置为value值。
- 如果没有需要释放或者没有找到已存在替换的,那么会 在检索到下标i的位置创建Entry,
tab[i] = new Entry(key, value);
- 判断是否需要扩容
ThreadLocal 缺陷
- 为什么在一个set方法,需要做这么多的事?
因为ThreadLocal的 Entry 的key为WeakReference,但是value 为强引用。那么当ThreadLocal这个key被回收之后,value无法被回收,也无法被引用,造成了内存泄漏。
所以ThreadLocal针对这个做了很多的事,先遍历,发现key为null,说明进行gc了,就遍历所有数组下标,将所有key为null且Entry 不为null的 Entry 清理掉。
Java 推荐我们将 ThreadLocal
设置为 static,并且用完就remove掉,来避免内存泄漏。
- ThreadLocal为了维护一个 ThreadLocalMap,hash寻址方法为
threadLocalHashCode & (INITIAL_CAPACITY - 1)
,但是hash冲突后方式为线性探测法,即往后面一直找,其实这样效率并不高(劣于HashMap的拉链法),极端情况下,要遍历整个 Entry 数组。
FastThreadLocal
基于上面对 ThreadLocal
分析,netty祭出了 FastThreadLocal
明显为了说明,我就是优于你。
FastThreadLocalThread
需要配合 FastThreadLocalThread
来使用,因为netty无法去更改jdk,所以netty创建一个子类来存储本地线程变量:
public class FastThreadLocalThread extends Thread {
private InternalThreadLocalMap threadLocalMap;
结合ThreadLocal,总体存储原理到这里,也能猜出个大概。
FastThreadLocal 使用
** get() 方法**
public final V get() {
InternalThreadLocalMap threadLocalMap = InternalThreadLocalMap.get();
Object v = threadLocalMap.indexedVariable(index);
if (v != InternalThreadLocalMap.UNSET) {
return (V) v;
}
return initialize(threadLocalMap);
}
InternalThreadLocalMap
的get
方法:
public static InternalThreadLocalMap get() {
Thread thread = Thread.currentThread();
if (thread instanceof FastThreadLocalThread) {
return fastGet((FastThreadLocalThread) thread);
} else {
return slowGet();
}
}
很容易理解出,如果是 FastThreadLocalThread
则从threadLocalMap
中取,否则从Thread 的变量中取。
2. Object v = threadLocalMap.indexedVariable(index);
寻址过程,就这么简单?
看index从哪里来?
private final int index;
public FastThreadLocal() {
index = InternalThreadLocalMap.nextVariableIndex();
}
每个 FastThreadLocal
都有一个index,变量从静态全局类 InternalThreadLocalMap
中获取,
InternalThreadLocalMap
的 nextVariableIndex
中获取:
public static int nextVariableIndex() {
int index = nextIndex.getAndIncrement();
if (index < 0) {
nextIndex.decrementAndGet();
throw new IllegalStateException("too many thread-local indexed variables");
}
return index;
}
nextIndex 为 AtomicInteger 原子更新类。
所以 FastThreadLocal
很简单,没有寻址过程,每个 FastThreadLocal
都有自己唯一的 index,不会存在hash冲突,真是一个优雅的设计方案!
3. 如果没有找到值,则调用 initialize(threadLocalMap);
方法,初始化一份数据并将本地线程 ,然后填充待删除一些信息:
private V initialize(InternalThreadLocalMap threadLocalMap) {
V v = null;
try {
v = initialValue(); // 初始化
} catch (Exception e) {
PlatformDependent.throwException(e);
}
threadLocalMap.setIndexedVariable(index, v); // 设置值
addToVariablesToRemove(threadLocalMap, this); // 记录清理的下标
return v;
}
记录完值后,需要记录清理当前 threadLocal的下标,主要用于清理时使用。
private static void addToVariablesToRemove(InternalThreadLocalMap threadLocalMap, FastThreadLocal<?> variable) {
Object v = threadLocalMap.indexedVariable(variablesToRemoveIndex);
Set<FastThreadLocal<?>> variablesToRemove;
if (v == InternalThreadLocalMap.UNSET || v == null) {
variablesToRemove = Collections.newSetFromMap(new IdentityHashMap<FastThreadLocal<?>, Boolean>());
threadLocalMap.setIndexedVariable(variablesToRemoveIndex, variablesToRemove);
} else {
variablesToRemove = (Set<FastThreadLocal<?>>) v;
}
variablesToRemove.add(variable);
}
- 将该FastThreadLocal信息,封装填充到
variablesToRemoveIndex
位置的InternalThreadLocalMap
中。
填充用来干啥呢?
当然是来 清除时使用的:
清除方式
有两种清除方式: - 手动执行remove或者removeAll。
- netty中,
FastThreadLocalRunnable
在run方法中嵌套了逻辑,在执行完后,会调用FastThreadLocal.removeAll();
方法进行清理。
@Override
public void run() {
try {
runnable.run();
} finally {
FastThreadLocal.removeAll();
}
}
总结
FastThreadLocal 相比于ThreadLoca,有以下优点:
- 没有弱引用,不用担心内存泄漏,但是需要手动remove,或执行removeAll方法。
- 使用数组替代Map,减少寻址和hash冲突,快!
关注博主公众号: 六点A君。
哈哈哈,一起研究Netty: