深入理解 Netty FastThreadLocal

2023年 10月 19日 16.7k 0

一、前言

最近在学习Netty相关的知识,在看到Netty FastThreadLocal章节中,回想起一起线上诡异问题。

问题描述:外销业务获取用户信息判断是否支持https场景下,获取的用户信息有时候竟然是错乱的。

问题分析:使用ThreadLocal保存用户信息时,未能及时进行remove()操作,而Tomcat工作线程是基于线程池的,会出现线程重用情况,所以获取的用户信息可能是之前线程遗留下来的。

问题修复:ThreadLocal使用完之后及时remove()、ThreadLocal使用之前也进行remove()双重保险操作。

接下来,我们继续深入了解下JDK ThreadLocal和Netty FastThreadLocal吧。

二、JDK ThreadLocal介绍

ThreadLocal是JDK提供的一个方便对象在本线程内不同方法中传递、获取的类。用它定义的变量,仅在本线程中可见,不受其他线程的影响,与其他线程相互隔离。

那具体是如何实现的呢?如图1所示,每个线程都会有个ThreadLocalMap实例变量,其采用懒加载的方式进行创建,当线程第一次访问此变量时才会去创建。

ThreadLocalMap使用线性探测法存储ThreadLocal对象及其维护的数据,具体操作逻辑如下:

假设有一个新的ThreadLocal对象,通过hash计算它应存储的位置下标为x。

此时发现下标x对应位置已经存储了其他的ThreadLocal对象,则它会往后寻找,步长为1,下标变更为x+1。

接下来发现下标x+1对应位置也已经存储了其他的ThreadLocal对象,同理则它会继续往后寻找,下标变更为x+2。

直到寻找到下标为x+3时发现是空闲的,然后将该ThreadLocal对象及其维护的数据构建一个entry对象存储在x+3位置。

在ThreadLocalMap中数据很多的情况下,很容易出现hash冲突,解决冲突需要不断的向下遍历,该操作的时间复杂度为O(n),效率较低。

图1

从下面的代码中可以看出:

Entry 的 key 是弱引用,value 是强引用。在 JVM 垃圾回收时,只要发现弱引用的对象,不管内存是否充足,都会被回收。

但是当 ThreadLocal 不再使用被 GC 回收后,ThreadLocalMap 中可能出现 Entry 的 key 为 NULL,那么 Entry 的 value 一直会强引用数据而得不到释放,只能等待线程销毁,从而造成内存泄漏。

static class ThreadLocalMap {
    // 弱引用,在资源紧张的时候可以回收部分不再引用的ThreadLocal变量
    static class Entry extends WeakReference> variablesToRemove;
        if (v == InternalThreadLocalMap.UNSET || v == null) {
            // 下标index为0的数据为空,则创建FastThreadLocal对象Set集合
            variablesToRemove = Collections.newSetFromMap(new IdentityHashMap>) v;
        }
        // 将FastThreadLocal对象保存到待清理的Set中
        variablesToRemove.add(variable);
    }
    // 省略其他代码
}
public final class InternalThreadLocalMap extends UnpaddedInternalThreadLocalMap {
    // 未赋值的Object变量(缺省值),当⼀个与线程绑定的值被删除之后,会被设置为UNSET
    public static final Object UNSET = new Object();
    // 存储绑定到当前线程的数据的数组
    private Object[] indexedVariables;
    // 绑定到当前线程的数据的数组能再次采用x2扩容的最大量
    private static final int ARRAY_LIST_CAPACITY_EXPAND_THRESHOLD = 1 >>  1;
            newCapacity |= newCapacity >>>  2;
            newCapacity |= newCapacity >>>  4;
            newCapacity |= newCapacity >>>  8;
            newCapacity |= newCapacity >>> 16;
            newCapacity ++;
        } else { // 不支持x2方式扩容,则设置绑定到当前线程的数据的数组容量为最大值
            newCapacity = ARRAY_LIST_CAPACITY_MAX_SIZE;
        }
        // 按扩容后的大小创建新数组,并将老数组数据copy到新数组
        Object[] newArray = Arrays.copyOf(oldArray, newCapacity);
        // 新数组扩容后的部分赋UNSET缺省值
        Arrays.fill(newArray, oldCapacity, newArray.length, UNSET);
        // 新数组的index位置替换成新的value
        newArray[index] = value;
        // 绑定到当前线程的数据的数组用新数组替换
        indexedVariables = newArray;
    }
    // 省略其他代码
}

源码中 set() 方法主要分为下面3个步骤处理:

判断value是否是缺省值UNSET,如果value不等于缺省值,则会通过InternalThreadLocalMap.get()方法获取当前线程的InternalThreadLocalMap,具体实现3.2小节中get()方法已做讲解。
通过FastThreadLocal中的setKnownNotUnset()方法将InternalThreadLocalMap中数据替换为新的value,并将当前的FastThreadLocal对象保存到待清理的Set中。
如果等于缺省值UNSET或null(else的逻辑),会调用remove()方法,remove()具体见后面的代码分析。

接下来我们看下

InternalThreadLocalMap.setIndexedVariable方法的实现逻辑。

判断index是否超出存储绑定到当前线程的数据的数组indexedVariables的长度,如果没有超出,则获取index位置的数据,并将该数组index位置数据设置新value。

如果超出了,绑定到当前线程的数据的数组需要扩容,则扩容该数组并将它index位置的数据设置新value。

扩容数组以index 为基准进行扩容,将数组扩容后的容量向上取整为 2 的次幂。然后将原数组内容拷贝到新的数组中,空余部分填充缺省值UNSET,最终把新数组赋值给 indexedVariables。

下面我们再继续看下

FastThreadLocal.addToVariablesToRemove方法的实现逻辑。

1.取下标index为0的数据(用于存储待清理的FastThreadLocal对象Set集合中),如果该数据是缺省值UNSET或null,则会创建FastThreadLocal对象Set集合,并将该Set集合填充到下标index为0的数组位置。

2.如果该数据不是缺省值UNSET,说明Set集合已金被填充,直接强转获取该Set集合。

3.最后将FastThreadLocal对象保存到待清理的Set集合中。

4.4 remove、removeAll方法

public class FastThreadLocal {
// FastThreadLocal初始化时variablesToRemoveIndex被赋值为0
private static final int variablesToRemoveIndex = InternalThreadLocalMap.nextVariableIndex();
public final void remove() {
// 获取当前线程的InternalThreadLocalMap
// 删除当前的FastThreadLocal对象及其维护的数据
remove(InternalThreadLocalMap.getIfSet());
}
public final void remove(InternalThreadLocalMap threadLocalMap) {
if (threadLocalMap == null) {
return;
}
// 根据当前线程的index,并将该数组下标index位置对应的值设置为缺省值UNSET
Object v = threadLocalMap.removeIndexedVariable(index);
// 存储待清理的FastThreadLocal对象Set集合中删除当前FastThreadLocal对象
removeFromVariablesToRemove(threadLocalMap, this);
if (v != InternalThreadLocalMap.UNSET) {
try {
// 空方法,用户可以继承实现
onRemoval((V) v);
} catch (Exception e) {
PlatformDependent.throwException(e);
}
}
}
public static void removeAll() {
InternalThreadLocalMap threadLocalMap = InternalThreadLocalMap.getIfSet();
if (threadLocalMap == null) {
return;
}
try {
// 取下标index为0的数据,用于存储待清理的FastThreadLocal对象Set集合中
Object v = threadLocalMap.indexedVariable(variablesToRemoveIndex);
if (v != null && v != InternalThreadLocalMap.UNSET) {
@SuppressWarnings("unchecked")
Set[] variablesToRemoveArray =
variablesToRemove.toArray(new FastThreadLocal[0]);
for (FastThreadLocal tlv: variablesToRemoveArray) {
tlv.remove(threadLocalMap);
}
}
} finally {
// 删除InternalThreadLocalMap中threadLocalMap和slowThreadLocalMap数据
InternalThreadLocalMap.remove();
}
}
private static void removeFromVariablesToRemove(
InternalThreadLocalMap threadLocalMap, FastThreadLocal variable) {
// 取下标index为0的数据,用于存储待清理的FastThreadLocal对象Set集合中
Object v = threadLocalMap.indexedVariable(variablesToRemoveIndex);

if (v == InternalThreadLocalMap.UNSET || v == null) {
return;
}
@SuppressWarnings("unchecked")
// 存储待清理的FastThreadLocal对象Set集合中删除该FastThreadLocal对象
Set

相关文章

JavaScript2024新功能:Object.groupBy、正则表达式v标志
PHP trim 函数对多字节字符的使用和限制
新函数 json_validate() 、randomizer 类扩展…20 个PHP 8.3 新特性全面解析
使用HTMX为WordPress增效:如何在不使用复杂框架的情况下增强平台功能
为React 19做准备:WordPress 6.6用户指南
如何删除WordPress中的所有评论

发布评论