Netty源码解析---FastThreadLocal-addToVariablesToRemove方法详解

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,就把它记录下来,方便以后统一清理。

二、用生活例子理解

想象你去图书馆借书:

  1. 借书时 :图书管理员会在你的借书卡上记录你借了哪些书
  2. 还书时:管理员看着借书卡,知道你要还哪些书

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] 位置是空的(UNSETnull),说明这是第一次使用,需要创建一个新的"借书卡"(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 的数据
    }
}

白话翻译:

  1. 看看"借书卡"(indexedVariables[0])上记录了哪些 FastThreadLocal
  2. 把这些 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()

相关推荐
CDA数据分析师干货分享2 小时前
【访谈】食品专业转行数据分析师,CDA数据分析师二级备考经验
学习·信息可视化·数据分析·cda证书·cda数据分析师
小信丶2 小时前
Spring MVC @SessionAttributes 注解详解:用法、场景与实战示例
java·spring boot·后端·spring·mvc
no24544102 小时前
深度解析:WebP会在几年内取代JPG吗?
java·大数据·人工智能·科技·ai
盐城吊霸天2 小时前
Spring AI + Flux/FluxSink + SSE 实战技术笔记
人工智能·笔记·spring
猿汁猿味yyds2 小时前
java学习day-15 集合、ArrayList集合
学习
NaclarbCSDN2 小时前
User ID controlled by request parameter with password disclosure-Burp 复现
网络·安全·web安全
William Dawson2 小时前
【Java Stream 流:高效、优雅的集合操作 ✨】
java·windows·python
疯狂成瘾者2 小时前
SseEmitter
java
solicitous2 小时前
遇到一个口头机遇的答辩准备4(ai给的术语清单)
学习·生活