作者:三哥,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 是否为空,如果为空则结束该方法。接着主要做了两件事情:
- 移除 map 中数组下标为 index 中的值
- 移除 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 的东西在自己项目中瞎搞,那就没办法往下聊了。
所以总结一下就是:合理情况下使用,确有空间浪费,但拿这点空间浪费换来的是几倍的效率提高,可取。