FastThreadLocal addToVariablesToRemove 方法详解
一、先理解要解决的问题
问题场景
假设你在一个线程中使用了多个 FastThreadLocal:
java
// 在某个线程中
FastThreadLocal<String> name = new FastThreadLocal<>();
name.set("张三");
FastThreadLocal<Integer> age = new FastThreadLocal<>();
age.set(25);
FastThreadLocal<String> city = new FastThreadLocal<>();
city.set("北京");
现在问题来了:当这个线程要结束或被复用时,怎么知道要清理哪些 FastThreadLocal?
你可能会说:我知道啊,清理 name、age、city 这三个就行了。
但是,在实际代码中,Netty 的框架代码并不知道你创建了哪些 FastThreadLocal!它需要一个机制来自动记录 这个线程用过哪些 FastThreadLocal。
解决方案
addToVariablesToRemove 方法就是用来记录 的:每当你在某个线程中使用一个 FastThreadLocal,就把它记录下来,方便以后统一清理。
二、用生活例子理解
想象你去图书馆借书:
- 借书时 :图书管理员会在你的借书卡上记录你借了哪些书
- 还书时:管理员看着借书卡,知道你要还哪些书
addToVariablesToRemove 就是"在借书卡上记录"这个动作。
三、数据存储在哪里?
每个线程都有一个 InternalThreadLocalMap,里面有一个数组 indexedVariables:
线程的 indexedVariables 数组:
┌─────────────────────────────────────┐
│ [0] → 一个 Set 集合(借书卡) │ ← 专门用来记录用过哪些 FastThreadLocal
│ [1] → name 的值 "张三" │ ← 第一个 FastThreadLocal 的数据
│ [2] → age 的值 25 │ ← 第二个 FastThreadLocal 的数据
│ [3] → city 的值 "北京" │ ← 第三个 FastThreadLocal 的数据
└─────────────────────────────────────┘
关键点:
- 数组的
[0]位置不存数据,而是存一个 Set 集合 - 这个 Set 集合里装的是:这个线程用过的所有
FastThreadLocal对象本身(不是值)
四、逐行解释代码
完整代码
java
private static void addToVariablesToRemove(
InternalThreadLocalMap threadLocalMap, // 当前线程的 Map
FastThreadLocal<?> variable) { // 要记录的 FastThreadLocal 对象
// 第 1 步:从数组的 [0] 位置取出 Set 集合
Object v = threadLocalMap.indexedVariable(variablesToRemoveIndex);
Set<FastThreadLocal<?>> variablesToRemove;
// 第 2 步:判断 Set 集合是否已经创建
if (v == InternalThreadLocalMap.UNSET || v == null) {
// 情况 1:第一次使用,还没有 Set,需要创建一个
variablesToRemove = Collections.newSetFromMap(
new IdentityHashMap<FastThreadLocal<?>, Boolean>()
);
// 把新创建的 Set 放到数组的 [0] 位置
threadLocalMap.setIndexedVariable(variablesToRemoveIndex, variablesToRemove);
} else {
// 情况 2:已经有 Set 了,直接用
variablesToRemove = (Set<FastThreadLocal<?>>) v;
}
// 第 3 步:把当前的 FastThreadLocal 对象加入 Set
variablesToRemove.add(variable);
}
第 1 步详解
java
Object v = threadLocalMap.indexedVariable(variablesToRemoveIndex);
白话翻译: 去数组的 [0] 位置看看,有没有那个"借书卡"(Set 集合)。
variablesToRemoveIndex的值永远是0- 这一步就是取
indexedVariables[0]
第 2 步详解
java
if (v == InternalThreadLocalMap.UNSET || v == null) {
// 创建新的 Set
variablesToRemove = Collections.newSetFromMap(
new IdentityHashMap<FastThreadLocal<?>, Boolean>()
);
threadLocalMap.setIndexedVariable(variablesToRemoveIndex, variablesToRemove);
} else {
// 使用已有的 Set
variablesToRemove = (Set<FastThreadLocal<?>>) v;
}
白话翻译:
- 如果
[0]位置是空的(UNSET或null),说明这是第一次使用,需要创建一个新的"借书卡"(Set) - 如果
[0]位置已经有东西了,说明之前已经创建过"借书卡"了,直接拿来用
为什么用 IdentityHashMap?
- 普通的 HashMap 用
equals()判断两个对象是否相同 - IdentityHashMap 用
==判断,也就是判断是不是同一个对象(内存地址相同) - 这里我们要记录的是
FastThreadLocal对象本身,不是它的值
第 3 步详解
java
variablesToRemove.add(variable);
白话翻译: 把当前这个 FastThreadLocal 对象记录到"借书卡"上。
五、完整执行流程示例
代码示例
java
// 假设在线程 A 中执行以下代码
// 1. 创建第一个 FastThreadLocal
FastThreadLocal<String> name = new FastThreadLocal<>();
name.set("张三"); // 这里会调用 addToVariablesToRemove
执行过程:
调用 name.set("张三")
↓
触发 addToVariablesToRemove(threadLocalMap, name)
↓
第 1 步:v = threadLocalMap.indexedVariable(0) // 取 [0] 位置
结果:v = UNSET(因为是第一次)
↓
第 2 步:v == UNSET,所以创建新的 Set
variablesToRemove = 新的 Set{}
把 Set 放到 [0] 位置
↓
第 3 步:variablesToRemove.add(name)
现在 Set = {name}
此时数组状态:
indexedVariables[0] = Set{name}
indexedVariables[1] = "张三"
继续添加第二个
java
// 2. 创建第二个 FastThreadLocal
FastThreadLocal<Integer> age = new FastThreadLocal<>();
age.set(25); // 这里会再次调用 addToVariablesToRemove
执行过程:
调用 age.set(25)
↓
触发 addToVariablesToRemove(threadLocalMap, age)
↓
第 1 步:v = threadLocalMap.indexedVariable(0)
结果:v = Set{name}(已经有了)
↓
第 2 步:v != UNSET,直接用已有的 Set
variablesToRemove = Set{name}
↓
第 3 步:variablesToRemove.add(age)
现在 Set = {name, age}
此时数组状态:
indexedVariables[0] = Set{name, age}
indexedVariables[1] = "张三"
indexedVariables[2] = 25
六、这个方法的作用是什么?
作用:记录"借书卡"
每次你在线程中使用一个 FastThreadLocal,这个方法就把它记录到 indexedVariables[0] 这个 Set 里。
为什么要记录?
因为后面有个 removeAll() 方法需要清理:
java
public static void removeAll() {
InternalThreadLocalMap threadLocalMap = InternalThreadLocalMap.getIfSet();
// 第 1 步:从 [0] 位置取出 Set(借书卡)
Object v = threadLocalMap.indexedVariable(variablesToRemoveIndex);
Set<FastThreadLocal<?>> variablesToRemove = (Set<FastThreadLocal<?>>) v;
// 第 2 步:遍历 Set,清理每一个 FastThreadLocal
for (FastThreadLocal<?> ftl : variablesToRemove) {
ftl.remove(threadLocalMap); // 清理这个 FastThreadLocal 的数据
}
}
白话翻译:
- 看看"借书卡"(
indexedVariables[0])上记录了哪些FastThreadLocal - 把这些
FastThreadLocal的数据一个个清理掉
七、总结
一句话总结
addToVariablesToRemove 方法就是:在数组的 [0] 位置维护一个 Set,记录当前线程用过哪些 FastThreadLocal 对象,方便以后统一清理。
类比理解
| 概念 | 类比 |
|---|---|
indexedVariables[0] |
借书卡 |
Set<FastThreadLocal<?>> |
借书卡上的书籍列表 |
addToVariablesToRemove |
在借书卡上记录借了哪本书 |
removeAll() |
看着借书卡还书 |
FastThreadLocal 对象 |
书本身(不是书的内容) |
FastThreadLocal 的值 |
书的内容 |
为什么需要这个机制?
在线程池环境中,线程会被复用。如果不清理旧数据:
- 会导致内存泄漏
- 下一个任务可能读到上一个任务的脏数据
通过这个"借书卡"机制,Netty 可以在任务结束时调用 removeAll(),一次性清理干净。
八、什么时候会调用这个方法?
时机 1:第一次调用 get() 方法
java
FastThreadLocal<String> name = new FastThreadLocal<>();
String value = name.get(); // 第一次 get,会触发 initialize()
内部流程:
java
public final V get() {
InternalThreadLocalMap threadLocalMap = InternalThreadLocalMap.get();
Object v = threadLocalMap.indexedVariable(index);
if (v != InternalThreadLocalMap.UNSET) {
return (V) v;
}
return initialize(threadLocalMap); // 第一次会走这里
}
private V initialize(InternalThreadLocalMap threadLocalMap) {
V v = initialValue(); // 获取初始值
threadLocalMap.setIndexedVariable(index, v); // 存储值
addToVariablesToRemove(threadLocalMap, this); // ← 在这里记录
return v;
}
时机 2:第一次调用 set() 方法
java
FastThreadLocal<String> name = new FastThreadLocal<>();
name.set("张三"); // 第一次 set
内部流程:
java
public final void set(V value) {
InternalThreadLocalMap threadLocalMap = InternalThreadLocalMap.get();
setKnownNotUnset(threadLocalMap, value);
}
private void setKnownNotUnset(InternalThreadLocalMap threadLocalMap, V value) {
if (threadLocalMap.setIndexedVariable(index, value)) {
// 返回 true 表示之前是 UNSET(第一次设置)
addToVariablesToRemove(threadLocalMap, this); // ← 在这里记录
}
}
注意: 只有第一次 set 才会调用 addToVariablesToRemove,后续的 set 不会重复调用。
九、图解完整流程
初始状态
线程 A 刚创建,还没使用任何 FastThreadLocal
indexedVariables 数组:
[0] → UNSET
[1] → UNSET
[2] → UNSET
...
第一步:创建并使用第一个 FastThreadLocal
java
FastThreadLocal<String> name = new FastThreadLocal<>(); // index = 1
name.set("张三");
执行 addToVariablesToRemove(threadLocalMap, name):
1. 取 indexedVariables[0],发现是 UNSET
2. 创建新的 Set{}
3. 把 Set 放到 indexedVariables[0]
4. Set.add(name)
结果:
[0] → Set{name}
[1] → "张三"
[2] → UNSET
...
第二步:创建并使用第二个 FastThreadLocal
java
FastThreadLocal<Integer> age = new FastThreadLocal<>(); // index = 2
age.set(25);
执行 addToVariablesToRemove(threadLocalMap, age):
1. 取 indexedVariables[0],发现已经有 Set{name}
2. 直接用这个 Set
3. Set.add(age)
结果:
[0] → Set{name, age}
[1] → "张三"
[2] → 25
[3] → UNSET
...
第三步:清理所有 FastThreadLocal
java
FastThreadLocal.removeAll();
执行流程:
1. 取 indexedVariables[0],得到 Set{name, age}
2. 遍历 Set:
- 调用 name.remove(),清理 indexedVariables[1]
- 调用 age.remove(),清理 indexedVariables[2]
结果:
[0] → UNSET
[1] → UNSET
[2] → UNSET
...
十、常见疑问解答
Q1:为什么要用 Set,不用 List?
A:因为 Set 可以自动去重。如果你多次调用 set(),不会重复添加同一个 FastThreadLocal。
Q2:为什么用 IdentityHashMap 而不是普通 HashMap?
A:
- 普通 HashMap:用
equals()判断相等,需要计算 hashCode - IdentityHashMap:用
==判断相等(比较内存地址),更快
我们要记录的是 FastThreadLocal 对象本身,不关心它的内容,所以用 == 就够了。
Q3:variablesToRemoveIndex 为什么是 0?
A:因为 FastThreadLocal 类加载时,第一个调用 nextVariableIndex(),所以得到 0。
java
public class FastThreadLocal<V> {
// 类加载时执行,得到 index = 0
private static final int variablesToRemoveIndex = InternalThreadLocalMap.nextVariableIndex();
}
Q4:如果我从不调用 removeAll(),会怎样?
A:
- 在普通线程中:线程结束时,整个
InternalThreadLocalMap会被 GC 回收,没问题 - 在线程池中:线程会被复用,旧数据不会被清理,导致内存泄漏和数据污染
所以 Netty 的 EventLoop 线程在每次任务结束后都会调用 removeAll()。