别只会ThreadLocal,FastThreadLocal也学起来

作者:三哥,j3code.cn

环境:JDK1.8、Java 源码版本 JDK8u、Linux 源码版本 Linux2.6.0、Netty4.1.101.Final 源码

视频地址:www.bilibili.com/video/BV1t5...


本篇讲述的是比 JDK 自带的 ThreadLocal 类效率还快的 FastThreadLocal ,他是 Netty 中提供的一个类,用于线程之间安全的传递数据,并对外隐藏 remove 操作,内部自动帮我们调用。

还不知道 ThreadLocal 的,建议去看一下我的这个视频:

www.bilibili.com/video/BV18s...

在没有分析 FastThreadLocal 之前,我们先思考一下 JDK 自带的 ThreadLocal 到底有哪些问题?

  • 存在 hash 冲突,解决办法是开放地址法,这种情况的时间复杂度 O(n)。
  • 存在内存泄漏问题,如果使用完的数据没有及时调用 remove 方法,会有此情况产生(这种情况是 Thread 生命周期很长,如线程池)。
  • get、set 等方法会存在多余的清除 key 为 null 的 Entry 情况,无形中给这些方法增加了负担。

Netty 中使用多线程的情况是非常多的,这就免不了需要线程安全的传递数据,而如果直接使用 ThreadLocal 肯定是可以达到这个效果的。但是效率上可就不保证非常高了,所以 Netty 就一不做二不休自己实现了一个快速的 ThreadLocal ,即 FastThreadLocal。它将刚刚我们例举出的问题,统统都进行了优化,是的,你没看错,就是统统都优化了,下面我们就来聊聊它。

1、使用

和 ThreadLocal 一样,我们可以在代码中直接通过构造器的方式使用它,就像下面这样:

csharp 复制代码
public class FastThreadLocalDemo02 {
    public static void main(String[] args) {
        // 创建 FastThreadLocal,带泛型
        FastThreadLocal<String> fastThreadLocal = new FastThreadLocal<>();

        fastThreadLocal.set("https://j3code.cn");

        new Thread(() -> {
            // 取不出来,一个 main 线程,一个 Thread-A 线程
            System.out.println(fastThreadLocal.get());
        },"Thread-A").start();
    }
}

虽然上面的使用没有什么问题,能达到多线程环境下安全的传递数据,但是你既然使用了 FastThreadLocal ,那么就要按照 Netty 提供的使用方式来使用。

直接使用 Thread 也能达到效果,但就是效率和 JDK 自带的没区别甚至可能还没 JDK 自带的高。

具体使用事项如下:

1)

不要使用 JDK 自带的 Thread 线程来使用 FastThreadLocal,也即 Netty 自定义了一个 FastThreadLocalThread ,是的你没看错,Netty 连 Thread 都自己定义了。

2)

虽然 FastThreadLocalThread 构造器接收的是 Runnable 类型对象,但是底层会将其包装为 FastThreadLocalRunnable 类型,这个类重写了 run 方法 自动调用 remove 方法,防止内存泄漏

正确使用代码案例:

csharp 复制代码
public class FastThreadLocalDemo02 {
    public static void main(String[] args) {
        // 创建 FastThreadLocal,带泛型
        FastThreadLocal<String> fastThreadLocal = new FastThreadLocal<>();

        // 规范使用
        FastThreadLocalThread fastThreadLocalThread = new FastThreadLocalThread(() -> {
            fastThreadLocal.set("我是J3code,这是我的个人网站:https://j3code.cn");
            System.out.println("fastThreadLocalThread线程存值成功!");
        },"Thread-A");
        fastThreadLocalThread.start();

        LockSupport.parkNanos(1 * 1000 * 1000 * 1000L);
        System.out.println("main线程获取值:" + fastThreadLocal.get());
    }
}

按理说介绍上述使用方法就够我们后面的源码分析了,但是我在多说一下 Netty 提供的通过线程工厂和线程执行器对象来使用 FastThreadLocal。

我们知道 JDK 中有 Thread 线程,所以 JDK 就为此提供了一系列的线程工厂和对应的执行器,如:PrivilegedThreadFactory 线程工厂、ThreadPoolExecutor 执行器。

所以,Netty 也一样为其自定义的线程 FastThreadLocalThread 提供了对应的线程工程和执行器,即:DefaultThreadFactory 线程工厂、ThreadPerTaskExecutor 执行器。

下面是结合线程工程 + 执行器,实现的 FastThreadLocal 案例代码:

csharp 复制代码
public class FastThreadLocalDemo02 {
    public static void main(String[] args) {
        // 创建 FastThreadLocal,带泛型
		FastThreadLocal<String> fastThreadLocal = new FastThreadLocal<>();

        // 创建线程工厂(这个工厂是专门创建 FastThreadLocalThread 线程的,
        // 将传入的 Runnable 对象包装成 FastThreadLocalRunnable 对象并传递给 FastThreadLocalThread 线程)
        DefaultThreadFactory fastThreadLocalTest = new DefaultThreadFactory("fastThreadLocalTest");
        // 创建 FastThreadLocalThread 线程的执行器,传入 FastThreadLocalThread 线程工厂
        ThreadPerTaskExecutor threadPerTaskExecutor = new ThreadPerTaskExecutor(fastThreadLocalTest);

        // 通过 FastThreadLocalThread 线程执行器执行 Runnable 对象
        threadPerTaskExecutor.execute(
                () -> {
                    fastThreadLocal.set("A");
                    System.out.println("fastThreadLocalTest-A: " + fastThreadLocal.get());
                }
        );

        threadPerTaskExecutor.execute(
                () -> {
                    System.out.println("fastThreadLocalTest-B: " + fastThreadLocal.get());
                }
        );
}

2、源码分析

在分析源码之前,我要再次提一下,通过 Thread 使用 FastThreadLocal 与通过 FastThreadLocalThread 使用 FastThreadLocal 情况会有所不同,到时候源码会有体现,这是为了打个预防针,防止看源码看的不明所以。

2.1 构造器

arduino 复制代码
private final int index;
public FastThreadLocal() {
    // 初始化 index 值
    index = InternalThreadLocalMap.nextVariableIndex();
}

在 FastThreadLocal 类中,有个 index 属性,可以看出该属性一旦赋值了就不能改变。其含义现在不解释,目前的话你可以理解为每个 FastThreadLocal 都唯一的对应一个 index 值。

nextVariableIndex:

java 复制代码
private static final AtomicInteger nextIndex = new AtomicInteger();
// index 最大值
private static final int ARRAY_LIST_CAPACITY_EXPAND_THRESHOLD = 1 << 30;
public static int nextVariableIndex() {
    // 获取原子类值并累加
    int index = nextIndex.getAndIncrement();
    // 如果大于最大值则报错
    if (index >= ARRAY_LIST_CAPACITY_MAX_SIZE || index < 0) {
        nextIndex.set(ARRAY_LIST_CAPACITY_MAX_SIZE);
        throw new IllegalStateException("too many thread-local indexed variables");
    }
    // 返回对应的值
    return index;
}

InternalThreadLocalMap 类中的一个公共静态方法,也即任何类都可以调用该方法。该方法通过原子整型类返回一个多线程环境下也能保证数据安全的 int 值(且唯一)。

以下文章如果出现"map"字样,一律就是指 InternalThreadLocalMap。

2.2 set 方法

scss 复制代码
public final void set(V value) {
	// 判断设置的值是否为 UNSET 对象
    if (value != InternalThreadLocalMap.UNSET) {
        // value 不是 UNSET 对象,则获取 map 对象,并调用setKnownNotUnset方法进行设值
        InternalThreadLocalMap threadLocalMap = InternalThreadLocalMap.get();
        setKnownNotUnset(threadLocalMap, value);
    } else {
        // 如果 set 的值是 UNSET ,则执行清理方法
        remove();
    }
}

set 方法通过 value 类型分为两步:如果 value 不是 UNSET 则调用后续方法进行 set,反之则执行移除方法。

UNSET 是 InternalThreadLocalMap 类中的一个静态常量,用于填充 indexedVariables 数组,表示对应下标处还未赋值。

indexedVariables 后续会分析到。

InternalThreadLocalMap.get()

下面来看看 InternalThreadLocalMap.get() 方法:

是不是和 ThreadLocal 很类似,都是先获取一个 map 对象,然后通过 map 对象来操作值,这里也是一样。

arduino 复制代码
public static InternalThreadLocalMap get() {
    // 获取当前线程对象
    Thread thread = Thread.currentThread();
    // 判断是否为 Netty 自定义线程类
    if (thread instanceof FastThreadLocalThread) {
        // 是,走快速获取 map 方法
        return fastGet((FastThreadLocalThread) thread);
    } else {
        // 不是,表面是传统 Thread 类,走慢的获取 map 方法
        return slowGet();
    }
}

从获取 InternalThreadLocalMap 对象方法就能看出,如果 FastThreadLocal 对象运行的环境是 Thread 则会走 slowGet() 方法;如果是 FastThreadLocalThread 则会走 fastGet() 方法,这也呼应了我一开始提到的使用的线程类不一样,对应的 FastThreadLocalThread 效果也会不一样。

fastGet

arduino 复制代码
private static InternalThreadLocalMap fastGet(FastThreadLocalThread thread) {
	// 从 FastThreadLocalThread 类中获取 InternalThreadLocalMap 属性变量
    InternalThreadLocalMap threadLocalMap = thread.threadLocalMap();
	// 如果为 null,则初始化
    if (threadLocalMap == null) {
        thread.setThreadLocalMap(threadLocalMap = new InternalThreadLocalMap());
    }
	// 不为空,直接返回
    return threadLocalMap;
}

要理解这个方法,我们要先看一下 FastThreadLocalThread 类中的一个属性

即每个 FastThreadLocalThread 类中都有一个 InternalThreadLocalMap 对象,用于存放数据,和 Thread 中的结构一样,只是类型不同。

理解了这点,那接下来就是初始化 InternalThreadLocalMap 对象并将值赋值给 FastThreadLocalThread 类中的属性了,我们来看看初始化的方法。

InternalThreadLocalMap 构造器

scss 复制代码
// 这个就是 InternalThreadLocalMap 类中存储数据的数组对象
private Object[] indexedVariables;
private InternalThreadLocalMap() {
    // 给这个数组初始化
    indexedVariables = newIndexedVariableTable();
}

newIndexedVariableTable:

typescript 复制代码
// 填充值,表示未使用,可以理解为占位符
public static final Object UNSET = new Object();
// 初始容量 32
private static final int INDEXED_VARIABLE_TABLE_INITIAL_SIZE = 32;
private static Object[] newIndexedVariableTable() {
    // 创建一个容量为 32 的数组
    Object[] array = new Object[INDEXED_VARIABLE_TABLE_INITIAL_SIZE];
    // 调用 Arrays 的填充方法,将数组的所有值设置为 UNSET
    Arrays.fill(array, UNSET);
    // 返回数组
    return array;
}

InternalThreadLocalMap 的创建和初始化还是很简单的,就是创建出 InternalThreadLocalMap 对象之后,再给 indexedVariables 赋一个大小为 32 的 Object 数组,且数组值都用 UNSET 占位符填充。

到这里,大家应该知道 FastThreadLocal 底层是个啥了吧?

一个 InternalThreadLocalMap ,InternalThreadLocalMap 里面一个 Object 数组

大家再回想一下 ThreadLocal 底层是啥?

一个 ThreadLocalMap,ThreadLocalMap 里面是个 Entry 数组,Entry 里面是个 key(弱引用) 为 ThreadLocal ,value 为 值 的键值对。

slowGet

csharp 复制代码
// 一个存储 InternalThreadLocalMap 对象的 ThreadLocal,这个属性是为了兼容直接通过 Thread 来
// 使用 FastThreadLocal 对象的情况
// 因为 FastThreadLocal 底层真正干活的是 InternalThreadLocalMap 而 Thread 中没有存储这个对象
// 的地方,所以只能绕个弯,通过 ThreadLocal 来缓存每个 Thread 对应的 InternalThreadLocalMap 对象
// 这样 Thread 类也能使用 FastThreadLocal 了。
private static final ThreadLocal<InternalThreadLocalMap> slowThreadLocalMap =
            new ThreadLocal<InternalThreadLocalMap>();
private static InternalThreadLocalMap slowGet() {
    // 通过 slowThreadLocalMap 获取 InternalThreadLocalMap 对象
    InternalThreadLocalMap ret = slowThreadLocalMap.get();
    if (ret == null) {
        // 没有,则创建一个,并存入 slowThreadLocalMap 中
        ret = new InternalThreadLocalMap();
        slowThreadLocalMap.set(ret);
    }
    // 直接返回
    return ret;
}

这个方法通过一个 ThreadLocal 来获取 InternalThreadLocalMap 对象,这样做的目的就是为了兼顾 Thread 类使用 FastThreadLocal 。

方法注释中我也说了,FastThreadLocal 底层干活的是 InternalThreadLocalMap,那么 Thread 中就必须要拿到这个对象,而我们知道 Thread 是肯定没有该对象的,所以需要为 Thread 与 InternalThreadLocalMap 做一个一对一映射,且保证线程安全。

Netty 的做法是通过 ThreadLocal 为每个 Thread 缓存一个 InternalThreadLocalMap 对象,就达到了 Thread 使用 FastThreadLocal 的目的。

但不推荐这样做哈,我编写这篇内容以来,一直强调要按照 Netty 提供给我们的方式进行使用,而不是这种兼容、绕弯的方式(效率低下,XXX都不用)。

setKnownNotUnset()

scss 复制代码
private void setKnownNotUnset(InternalThreadLocalMap threadLocalMap, V value) {
    // 根据 index 和获取到的 threadLocalMap 对象,直接通过下标设值
    if (threadLocalMap.setIndexedVariable(index, value)) {
        // true 则将当前 FastThreadLocal 对象存入 threadLocalMap 的第一个下标处
        addToVariablesToRemove(threadLocalMap, this);
    }
}

可以看到设值的底层是直接通过 InternalThreadLocalMap 的 setIndexedVariable 方法完成的,之后如果方法返回 true 则执行后续逻辑。

下面来看看设值方法:

typescript 复制代码
public boolean setIndexedVariable(int index, Object value) {
    // 定义临时变量,指向 indexedVariables
    Object[] lookup = indexedVariables;
    // 判断 index 是否超出数组长度
    if (index < lookup.length) {
        // 没有则直接复制
        Object oldValue = lookup[index];
        lookup[index] = value;
        // 将 旧值 与 UNSET 对比,一致:表面原先改地方没值,反之表示原先已经有值了,现在被你覆盖了
        return oldValue == UNSET;
    } else {
        // 扩容后,再设值值
        expandIndexedVariableTableAndSet(index, value);
        // 返回 true
        return true;
    }
}

这个方法分为两步:

1)如果 FastThreadLocal 对应的 index 在 InternalThreadLocalMap 对象的 indexedVariables 数组长度之内,则直接将值设值到 index 下标处,并对原先 index 下标的值与新设值的值作比较。如果一致,返回 true;反之返回 false。

2)条件 1)不满足表示数组该扩容了,不然放不下咯。所以调用 expandIndexedVariableTableAndSet 方法进行扩容在设置值,最后返回 true 。

这里可能大家有疑问,返回 true / false 表明什么含义,莫慌,我们继续往后面分析就知道了。

expandIndexedVariableTableAndSet

ini 复制代码
private void expandIndexedVariableTableAndSet(int index, Object value) {
    // 局部变量
    Object[] oldArray = indexedVariables;
    final int oldCapacity = oldArray.length;
    int newCapacity;
    // 根据传进来的 index 计算最新的数组容量,也即最近一个大于 index 的 幂次方值
    if (index < ARRAY_LIST_CAPACITY_EXPAND_THRESHOLD) {
        // 看看是不是有点熟悉(HashMap)
        newCapacity = index;
        newCapacity |= newCapacity >>>  1;
        newCapacity |= newCapacity >>>  2;
        newCapacity |= newCapacity >>>  4;
        newCapacity |= newCapacity >>>  8;
        newCapacity |= newCapacity >>> 16;
        newCapacity ++;
    } else {
        newCapacity = ARRAY_LIST_CAPACITY_MAX_SIZE;
    }

    // 调用 Arrays 方法创建一个新数组,并将旧值原封不动的移过去
    Object[] newArray = Arrays.copyOf(oldArray, newCapacity);
    // 没值的数组下标初始化为 UNSET
    Arrays.fill(newArray, oldCapacity, newArray.length, UNSET);
    // 将 value 设置到新数组中
    newArray[index] = value;
    // indexedVariables 指向新数组
    indexedVariables = newArray;
}

该方法先计算新数组的容量,然后通过 Arrays 生成新的数组并将旧值迁移到新数组中,接着将空下标值初始化为 UNSET ,最后在 index 处将 value 设值进去,最后将新数赋给 indexedVariables。

思考一下,通过 index 确定扩容后的数组大小,会不会存在浪费空间问题?

addToVariablesToRemove

typescript 复制代码
// 类加载的时候就会执行 nextVariableIndex 方法,给 VARIABLES_TO_REMOVE_INDEX 赋值,为 0
public static final int VARIABLES_TO_REMOVE_INDEX = nextVariableIndex();
private static void addToVariablesToRemove(InternalThreadLocalMap threadLocalMap, FastThreadLocal<?> variable) {
    // 根据下标获取值,VARIABLES_TO_REMOVE_INDEX 为常量 0
    Object v = threadLocalMap.indexedVariable(VARIABLES_TO_REMOVE_INDEX);
    // 定义一个 set 集合
    Set<FastThreadLocal<?>> variablesToRemove;
    // 如果 InternalThreadLocalMap 中下标为 0 的值为 UNSET 或者 null
    if (v == InternalThreadLocalMap.UNSET || v == null) {
        // 创建一个 set 类型集合
        variablesToRemove = Collections.newSetFromMap(new IdentityHashMap<FastThreadLocal<?>, Boolean>());
        // 将 set 集合类型设置到 indexedVariables 数组中的第一个位置 
        threadLocalMap.setIndexedVariable(VARIABLES_TO_REMOVE_INDEX, variablesToRemove);
    } else {
        // 下标为 0 处有值,强转为 set 类型
        variablesToRemove = (Set<FastThreadLocal<?>>) v;
    }
    // 项 set 集合中添加当前 FastThreadLocal 对象
    variablesToRemove.add(variable);
}

// 根据下标获取对应的值,如果下标超出数组长度返回 UNSET
public Object indexedVariable(int index) {
    Object[] lookup = indexedVariables;
    return index < lookup.length? lookup[index] : UNSET;
}

该方法的作用是将 FastThreadLocalThread 中 InternalThreadLocalMap 对象中的 indexedVariables 数组下标为 0 的位置设置为 set 集合,并将当前 FastThreadLocal 对象存入进去。

现在就能解释 setIndexedVariable 方法返回 true/false 的原因了?

因为 FastThreadLocal 设值完值后,会将当前 FastThreadLocal 对象存入到 indexedVariables 数组下标为 0 的 set 集合中。如果 FastThreadLocal 第一次调用 set 方法,那么它肯定就不在数组下标为 0 处的 set 集合中,所以需要返回 true 将其添加进去;反之 FastThreadLocal 多次调用 set 方法,那么它肯定就在数组下标为 0 的 set 集合中,所以需要返回 false 无需重复将其放进去。

2.3 get 方法

kotlin 复制代码
public final V get() {
    // 先获取当前线程的 map 对象
    InternalThreadLocalMap threadLocalMap = InternalThreadLocalMap.get();
    // 根据当前 FastThreadLocal 对象的 index 获取对应数组下标值
    Object v = threadLocalMap.indexedVariable(index);
    if (v != InternalThreadLocalMap.UNSET) {
        // 有值,强转返回出去
        return (V) v;
    }
    // 否则初始化值,并返回一个初始值出去
    return initialize(threadLocalMap);
}

该方法比较简单,先获取线程对应的 map 然后根据下标取值,有值则返回没值则初始化。下面我们看看初始化值的方法:

java 复制代码
private V initialize(InternalThreadLocalMap threadLocalMap) {
    V v = null;
    try {
        // 调用待子类实现的初始值方法,为实现时为 null
        v = initialValue();
        if (v == InternalThreadLocalMap.UNSET) {
            throw new IllegalArgumentException("InternalThreadLocalMap.UNSET can not be initial value.");
        }
    } catch (Exception e) {
        PlatformDependent.throwException(e);
    }

	// 在对应下标处设值
    threadLocalMap.setIndexedVariable(index, v);
	// 将当前 FastThreadLocal 对象设置到 map 中数组下标为 0 的 set 集合中
    addToVariablesToRemove(threadLocalMap, this);
	// 返回初始化的值
    return v;
}

// 空实现
protected V initialValue() throws Exception {
    return null;
}

get 方法比较简单,通过下标 index 去数组中获取值,时间复杂度为 O(1) ,比 JDK 提供的 ThreadLocal 效率高。当不存在值时,也和 JDK 提供的 ThreadLocal 类似,设置一个默认值进去(initialValue 方法提供的值)。

2.4 remove 方法

csharp 复制代码
public final void remove() {
    remove(InternalThreadLocalMap.getIfSet());
}

这个方法先获取 FastThreadLocal 的 map 对象如果没有则设值一个,然后调用 remove 重载方法进行移除数据。

InternalThreadLocalMap.getIfSet()

arduino 复制代码
public static InternalThreadLocalMap getIfSet() {
    // 获取当前线程对象
    Thread thread = Thread.currentThread();
    if (thread instanceof FastThreadLocalThread) {
        // 根据 FastThreadLocalThread 获取 map
        return ((FastThreadLocalThread) thread).threadLocalMap();
    }
    // 因为是普通 Thread ,所以需要访问 map 中的 ThreadLocal 类型属性进行获取
    return slowThreadLocalMap.get();
}

可以看到,就是一个简单的获取,如果没有的话直接返回 null 出去。

remove()

java 复制代码
public final void remove(InternalThreadLocalMap threadLocalMap) {
	// map 为 null 就结束
    if (threadLocalMap == null) {
        return;
    }
    // 调用 removeIndexedVariable 移除 index 处的值,并返回
    Object v = threadLocalMap.removeIndexedVariable(index);
    if (v != InternalThreadLocalMap.UNSET) {
        // 应为数组 index 处的值已经被移除了,所以缓存在 map 数组 0 位置 set 集合
        // 中的 FastThreadLocal 对象也要移除
        removeFromVariablesToRemove(threadLocalMap, this);
        try {
            // 子类实现,空方法
            onRemoval((V) v);
        } catch (Exception e) {
            PlatformDependent.throwException(e);
        }
    }
}
protected void onRemoval(@SuppressWarnings("UnusedParameters") V value) throws Exception { }

该方法先判断传入的 InternalThreadLocalMap 是否为空,如果为空则结束该方法。接着主要做了两件事情:

  1. 移除 map 中数组下标为 index 中的值
  2. 移除 map 中数组下标为 0 中 set 集合中的 FastThreadLocal 对象

下面来分别看看这两个方法:

removeIndexedVariable

perl 复制代码
public Object removeIndexedVariable(int index) {
	// 获取数组,index 小于数组长度,则将数组 index 下标处的值赋为 UNSET,并将旧值返回
	// index 大于数组长度,那就直接返回 UNSET
    Object[] lookup = indexedVariables;
    if (index < lookup.length) {
        Object v = lookup[index];
        lookup[index] = UNSET;
        return v;
    } else {
        return UNSET;
    }
}

该方法很简单,看我注释即可。

removeFromVariablesToRemove

typescript 复制代码
private static void removeFromVariablesToRemove(
    InternalThreadLocalMap threadLocalMap, FastThreadLocal<?> variable) {
    // 获取 map 中下标为 0 的值
    Object v = threadLocalMap.indexedVariable(VARIABLES_TO_REMOVE_INDEX);

    // 为空 直接结束
    if (v == InternalThreadLocalMap.UNSET || v == null) {
        return;
    }

    // 强转为 set 结合然后移除 FastThreadLocal 对象
    @SuppressWarnings("unchecked")
    Set<FastThreadLocal<?>> variablesToRemove = (Set<FastThreadLocal<?>>) v;
    variablesToRemove.remove(variable);
}

该方法也是很简单,先获取 map 中下标为 0 的 set 集合,如果不存在就直接结束;反之将值强转为 set 集合接着调用 set 的 remove 方法移除 FastThreadLocal 对象。

removeAll()

typescript 复制代码
public static void removeAll() {
    // 获取当前线程的 map
    InternalThreadLocalMap threadLocalMap = InternalThreadLocalMap.getIfSet();
    // 空就结束
    if (threadLocalMap == null) {
        return;
    }

    try {
        // 获取 map 数组中下标为 0 的 set 集合
        Object v = threadLocalMap.indexedVariable(VARIABLES_TO_REMOVE_INDEX);
        if (v != null && v != InternalThreadLocalMap.UNSET) {
            // 不为空,且不为 UNSET
            @SuppressWarnings("unchecked")
            // 强转为 set
            Set<FastThreadLocal<?>> variablesToRemove = (Set<FastThreadLocal<?>>) v;
            // 将 set 集合转为 FastThreadLocal 数组对象
            FastThreadLocal<?>[] variablesToRemoveArray =
            variablesToRemove.toArray(new FastThreadLocal[0]);
            // 遍历
            for (FastThreadLocal<?> tlv: variablesToRemoveArray) {
                // 调用每个 FastThreadLocal 对象的 remove 方法(移除 index 下标值 + set 集合值)
                tlv.remove(threadLocalMap);
            }
        }
    } finally {
        // 将当前线程中的 map 设为 null
        InternalThreadLocalMap.remove();
    }
}

该方法会将当前线程中设置的所有 FastThreadLocal 对象设置的值都移除,并且最后会将 map 设为 null。

这里就体现了为啥需要将 map 中的数组对象下标为 0 的地方存为 set 集合了(存有当前线程的所有 FastThreadLocal 对象)。当一个线程完成了业务逻辑之后,需要移除该线程中设置的所有 FastThreadLocal 对象,而当前线程中的 map 对象数组属性如果都拿来存值就无法实现这一点,会造成内存泄漏,所以就将数组的 0 下标用来存该线程操作的所有 FastThreadLocal 对象结合,就可以实现这点。还有 FastThreadLocal 的 size 方法也有体现,获取当前线程中存入的 FastThreadLocal 数量也可以通过 map 下标为 0 的 set 集合提供的方法(O(1))得到,而不用通过遍历数组才能获得(O(n))。

最后再来看看移除 map 方法:

arduino 复制代码
public static void remove() {
    // 获取当前线程对象
    Thread thread = Thread.currentThread();
    if (thread instanceof FastThreadLocalThread) {
        // 将 FastThreadLocalThread 类中的 map 设为 null
        ((FastThreadLocalThread) thread).setThreadLocalMap(null);
    } else {
        // 移除 ThreadLocal 中 thread 与 map 的映射
        slowThreadLocalMap.remove();
    }
}

方法很简单,如果为 FastThreadLocalThread 线程,那么直接将线程中的 map 属性设为 null,反之则移除 map 中 ThreadLocal 中 Thread 与 map 的映射关系即可(目的:让 Thread 找不到 map)。

3、对比

对于 ThreadLocal 与 FastThreadLocalThread 的性能,在 Netty 中也提供了对应的测试类,就是下面两个:

  • io.netty.microbench.concurrent.FastThreadLocalFastPathBenchmark
  • io.netty.microbench.concurrent.FastThreadLocalSlowPathBenchmark

第一个:表示通过 FastThreadLocalThread 来使用 FastThreadLocal 与 Thread 来使用 ThreadLocal 的实验。

第二个:表示通过 Thread 来使用 FastThreadLocal 与 Thread 来使用 ThreadLocal 的实验。

大家可以看我本机上的效率对比结果图:

1):

2):

从我本机中的结果可以看出 FastThreadLocalThread 使用 FastThreadLoca 确实比 JDK 提供的快,且通过 Thread 使用 FastThreadLoca 则比 JDK 提供的还慢。

不过我这里的性能体现没有出现几倍的效果,不知道原因是啥,大家可以看一下我从网上截图被人测试的结果,确实能看到有 3 倍多的提升。

实验结论:只有使用 FastThreadLocalThread 线程来操作 FastThreadLocal 才会快,而如果是普通线程操作 FastThreadLocal 还比 JDK 自带的更慢。

💡如果大家 Netty 源码运行出现下面情况,可以看看:

运行 FastThreadLocalFastPathBenchmark 和 FastThreadLocalSlowPathBenchmark 出现类似 Unrecognized option: --illegal-access=deny 错误时,解决办法是将 netty-parent 根项目的 pom.xml 中下面内容注释即可

java11

11

<argLine.java9.extras />

<argLine.java9>--illegal-access=deny ${argLine.java9.extras}</argLine.java9>

<argLine.alpnAgent />

<forbiddenapis.skip>true</forbiddenapis.skip>

<jboss.marshalling.version>2.0.5.Final</jboss.marshalling.version>

true

4、整个流程图

1)原始 Thread + ThreadLocal 流程

2)原始 Thread + FastThreadLocal 流程及 FastThreadLocalThread + FastThreadLocal 流程

Q&A

文中遗留了一个问题:map 中的数组是根据 index 的大小进行扩容的,这会造成空间浪费

我说一下我的理解:

index 是 FastThreadLcal 的内部属性,即创建一个 FastThreadLocal 对象则会存在一个唯一的 index 值,如果有 100 个这个对象,index 也确实为 100。接着一个 FastThreadLocalThread 线程来运行这段业务代码,其内部的 map 中的数组毫无疑问会变成长度为 128 的 Obejct 数组。无形中浪费了 28 个位置,如果再多一些 129 个,FastThreadLocal 对象中的数组则浪费的更多。这还是一个线程,如果是 100 个这样的线程,那你算算这要浪费多少空间。

不过我们想一下,这是人家 Netty 编写的给自己用的东西,人家的代码中肯定没有定义这么多 FastThreadLocal 对象,并且 Netty 中都是通过线程组(可以理解线程池)的方式运行线程通常也就 CPU 核心数 * 2,所以也确保了 FastThreadLocalThread 不会太多。当然你要是非拿 Netty 的东西在自己项目中瞎搞,那就没办法往下聊了。

所以总结一下就是:合理情况下使用,确有空间浪费,但拿这点空间浪费换来的是几倍的效率提高,可取。

相关推荐
小码哥_常4 小时前
别再被误导!try...catch性能大揭秘
后端
无巧不成书02186 小时前
30分钟入门Java:从历史到Hello World的小白指南
java·开发语言
苍何7 小时前
30分钟用 Agent 搓出一家跨境网店,疯了
后端
ssshooter7 小时前
Tauri 2 iOS 开发避坑指南:文件保存、Dialog 和 Documents 目录的那些坑
前端·后端·ios
追逐时光者7 小时前
一个基于 .NET Core + Vue3 构建的开源全栈平台 Admin 系统
后端·.net
程序员飞哥7 小时前
90后大龄程序员失业4个月终于上岸了
后端·面试·程序员
zs宝来了8 小时前
Playwright 自动发布 CSDN 的完整实践
java
Cyeam8 小时前
爆火的 OpenClaw,赢在生态创新
程序员·开源·openai
吴声子夜歌9 小时前
TypeScript——基础类型(三)
java·linux·typescript
GetcharZp9 小时前
Git 命令行太痛苦?这款 75k Star 的神级工具,让你告别“合并冲突”恐惧症!
后端