JDK的ThreadLocal不行 | 还是来看看FastThreadLocal吧

518 阅读17分钟

作者:三哥,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 一样,我们可以在代码中直接通过构造器的方式使用它,就像下面这样:

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 方法,防止内存泄漏

正确使用代码案例:

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 案例代码:

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 构造器

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

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

nextVariableIndex:

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 方法

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 对象来操作值,这里也是一样。

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

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 构造器

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

newIndexedVariableTable:

// 填充值,表示未使用,可以理解为占位符
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

// 一个存储 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()

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

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

下面来看看设值方法:

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

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

// 类加载的时候就会执行 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 方法

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 然后根据下标取值,有值则返回没值则初始化。下面我们看看初始化值的方法:

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 方法

public final void remove() {
    remove(InternalThreadLocalMap.getIfSet());
}

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

InternalThreadLocalMap.getIfSet()

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()

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

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

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()

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 方法:

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 的东西在自己项目中瞎搞,那就没办法往下聊了。

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