ARTICLE · 1138829
Netty 4.2.x 源码深度解析 (二):FastThreadLocal —— Netty 自研线程局部变量

文章目录
一、JDK ThreadLocal 的痛点与 FastThreadLocal 的设计目标 二、InternalThreadLocalMap:基于数组的存储核心 三、set() 源码分析:按索引写入 四、get() 源码分析:按索引读取 + 懒初始化 五、remove() 与内存泄漏防护 六、FastThreadLocalThread:线程绑定与快速路径 七、整体链路串联 全文小结
common 模块是 Netty 整个框架的基石,为上层所有模块提供了 FastThreadLocal、Recycler、HashedWheelTimer 等基础设施。其中 FastThreadLocal 是最核心的线程局部变量实现,在高性能网络编程场景下,Channel、EventLoop、PooledByteBufAllocator 等核心类均依赖它存储线程级数据,每次 IO 读写都可能触发调用。
JDK 的 ThreadLocal 已经存在了二十多年,Netty 为什么还要自研一套 FastThreadLocal?它到底"快"在哪里?FastThreadLocal 如何用数组索引替代哈希表,实现 O(1) 无冲突访问?InternalThreadLocalMap 作为核心存储结构,它的 get()/set()/remove() 调用链路是怎样的?FastThreadLocalThread 和普通线程在访问路径上有什么不同?FastThreadLocalRunnable 如何保证线程池场景下的内存安全?FastThreadLocalThread 的虚拟线程兼容又是如何实现的?
本文将沿着 FastThreadLocal 的 set() → get() → remove() 调用链路,逐一分析数组索引机制、InternalThreadLocalMap 的快速路径与慢速路径、以及 FastThreadLocalThread 的线程绑定设计。
一、JDK ThreadLocal 的痛点与 FastThreadLocal 的设计目标
要理解 FastThreadLocal 的设计动机,首先需要回顾 JDK ThreadLocal 的核心原理。JDK ThreadLocal 的存储结构是 Thread → ThreadLocalMap → 哈希表。每个 Thread 实例持有一个 threadLocals 字段(类型为 ThreadLocalMap),而 ThreadLocalMap 内部是一个 Entry[] 数组,以 ThreadLocal 的哈希值为 key 进行线性探测寻址。当多个 ThreadLocal 的哈希值发生冲突时,需要通过线性探测找到下一个可用槽位,这意味着访问复杂度在最坏情况下会从 O(1) 退化到 O(n)。
JDK ThreadLocal 存在三大痛点:
一是哈希冲突导致性能退化。ThreadLocalMap 使用 ThreadLocal.threadLocalHashCode 作为哈希值,通过 key.threadLocalHashCode & (table.length - 1) 计算槽位。虽然 ThreadLocal 的哈希值通过 HASH_INCREMENT = 0x61c88647(黄金分割数)递增以保证均匀分布,但在 ThreadLocal 实例数量较多时仍可能发生冲突,需要线性探测解决。
二是弱引用 Key 引发内存泄漏风险。ThreadLocalMap.Entry 继承自 WeakReference<ThreadLocal<?>>,Key 是弱引用而 Value 是强引用。当 ThreadLocal 实例被 GC 回收后,Entry 的 Key 变为 null,但 Value 仍被 Entry 强引用持有。虽然 JDK 在 get()/set() 时会触发试探性清理(expungeStaleEntry),但若线程长期存活(如线程池中的线程),仍可能发生内存泄漏。
三是每次访问需计算哈希值。ThreadLocal.get() 需要先通过哈希计算定位槽位,然后对比 Key 是否匹配,不匹配时还需线性探测。在 Netty 这种高频 IO 场景下,每次 IO 读写都可能触发 FastThreadLocal 的读写操作,累积的哈希计算开销不可忽视。
Netty 的解决方案是 FastThreadLocal 的核心设计理念:用 AtomicInteger 分配的常量索引替代哈希值,数组下标直接定位,O(1) 且无冲突。每个 FastThreadLocal 实例在构造时从全局 AtomicInteger 中获取一个唯一递增的 index,这个 index 在整个 JVM 生命周期内是常量。访问时直接用 index 作为数组下标,无需计算哈希、无需探测冲突。

二、InternalThreadLocalMap:基于数组的存储核心
InternalThreadLocalMap 是 FastThreadLocal 的存储核心,它继承自 UnpaddedInternalThreadLocalMap。后者已标记为 @Deprecated,当前为空类,最初设计时作为无缓存行填充的基类,让 InternalThreadLocalMap 通过继承添加 rp1 到 rp8 共 8 个 long 填充字段来避免伪共享。这些填充字段也已标记为 @deprecated,将在未来版本移除。
InternalThreadLocalMap 最核心的字段是 Object[] indexedVariables,初始容量为 INDEXED_VARIABLE_TABLE_INITIAL_SIZE = 32。数组构造时通过 Arrays.fill(array, UNSET) 用 UNSET 哨兵对象填充每一个槽位。UNSET 是一个全局唯一的 new Object() 实例,用于区分"未设置"与 null 值,因为 FastThreadLocal 允许存储 null 值,不能用 null 表示"未初始化"。
io.netty.util.internal.InternalThreadLocalMap// 哨兵对象,用于区分"未设置"与 null 值public static final Object UNSET = new Object();// 为 FastThreadLocal 提供数组索引存储private Object[] indexedVariables;// 构造时创建初始容量 32 的数组,全部填充 UNSETprivate InternalThreadLocalMap() {indexedVariables = newIndexedVariableTable();}private static Object[] newIndexedVariableTable() {Object[] array = new Object[INDEXED_VARIABLE_TABLE_INITIAL_SIZE];Arrays.fill(array, UNSET);return array;}
索引分配机制是 FastThreadLocal 高性能的根源。nextVariableIndex() 方法通过全局 AtomicInteger nextIndex 的 getAndIncrement() 原子递增,每个 FastThreadLocal 实例在构造时获取唯一 index:
io.netty.util.internal.InternalThreadLocalMap#nextVariableIndex// 全局原子递增分配索引,每个 FastThreadLocal 获取唯一索引publicstaticintnextVariableIndex() {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;}
这意味着 FastThreadLocal 的构造器极其简单,仅调用 nextVariableIndex() 分配索引,除此之外不做任何初始化:
io.netty.util.concurrent.FastThreadLocalprivate final int index;publicFastThreadLocal() {index = InternalThreadLocalMap.nextVariableIndex();}
InternalThreadLocalMap 的获取过程是理解快速路径与慢速路径的关键。下面这张时序图展示了从 FastThreadLocal 构造到首次 set() 的完整调用链路:

上面的时序图清晰地展示了两个层次:索引分配在构造时一次性完成,不涉及任何线程;InternalThreadLocalMap 的获取在首次 set() 时触发,根据当前线程类型分叉到快速路径或慢速路径。接下来我们逐一分析 set()、get() 和 remove() 的源码实现。
三、set() 源码分析:按索引写入
FastThreadLocal#set() 的入口非常简洁,直接委托给 getAndSet():
io.netty.util.concurrent.FastThreadLocal#setpublicfinalvoidset(V value){getAndSet(value);}
getAndSet() 是写入的核心调度方法。它先判断 value 是否为 UNSET 哨兵值,如果是,走 removeAndGet() 路径(因为 UNSET 是"未初始化"的标记,不允许作为有效值存储);否则走正常写入路径:
io.netty.util.concurrent.FastThreadLocal#getAndSetpublic V getAndSet(V value) {if (value != InternalThreadLocalMap.UNSET) {// 正常写入路径:获取 InternalThreadLocalMap,按索引写入InternalThreadLocalMap threadLocalMap = InternalThreadLocalMap.get();return setKnownNotUnset(threadLocalMap, value);}// UNSET 是哨兵值,不允许作为有效值存储,走删除路径return removeAndGet(InternalThreadLocalMap.getIfSet());}
setKnownNotUnset() 是实际写入数据的方法。它调用 threadLocalMap.getAndSetIndexedVariable(index, value) 将值写入数组的索引位置,并返回旧值。如果旧值是 UNSET(说明这是首次写入),则调用 addToVariablesToRemove() 将当前 FastThreadLocal 实例注册到 VARIABLES_TO_REMOVE_INDEX 位置:
io.netty.util.concurrent.FastThreadLocal#setKnownNotUnset@SuppressWarnings("unchecked")private V setKnownNotUnset(InternalThreadLocalMap threadLocalMap, V value) {V old = (V) threadLocalMap.getAndSetIndexedVariable(index, value);if (old == UNSET) {// 首次写入:注册到 VARIABLES_TO_REMOVE_INDEX,用于后续统一清理addToVariablesToRemove(threadLocalMap, this);return null;}return old;}
InternalThreadLocalMap#getAndSetIndexedVariable() 是数组写入的底层实现。它先检查索引是否在数组范围内,如果超出则触发扩容,否则直接写入:
io.netty.util.internal.InternalThreadLocalMap#getAndSetIndexedVariablepublic Object getAndSetIndexedVariable(int index, Object value) {Object[] lookup = indexedVariables;if (index < lookup.length) {// 索引在范围内,直接写入并返回旧值Object oldValue = lookup[index];lookup[index] = value;return oldValue;}// 索引超出数组长度,触发扩容expandIndexedVariableTableAndSet(index, value);return UNSET;}
数组扩容策略值得关注。expandIndexedVariableTableAndSet() 通过 |= >>> 1 / |= >>> 2 / |= >>> 4 / |= >>> 8 / |= >>> 16 这一系列位运算,将目标索引的计算值向上取整到最近的 2 的幂次。例如 index=40 时,经位运算后 newCapacity=64:
io.netty.util.internal.InternalThreadLocalMap#expandIndexedVariableTableAndSetprivatevoidexpandIndexedVariableTableAndSet(int index, Object value) {Object[] oldArray = indexedVariables;final int oldCapacity = oldArray.length;int newCapacity;if (index < ARRAY_LIST_CAPACITY_EXPAND_THRESHOLD) {// 位运算向上取整到 2 的幂次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;}// 拷贝旧数组,新槽位填充 UNSETObject[] newArray = Arrays.copyOf(oldArray, newCapacity);Arrays.fill(newArray, oldCapacity, newArray.length, UNSET);newArray[index] = value;indexedVariables = newArray;}
四、get() 源码分析:按索引读取 + 懒初始化
FastThreadLocal#get() 的读取逻辑同样简洁。它先获取当前线程的 InternalThreadLocalMap,然后通过 indexedVariable(index) 直接按数组下标读取。如果读到 UNSET(说明从未设置过),则触发 initialize() 懒初始化:
io.netty.util.concurrent.FastThreadLocal#get@SuppressWarnings("unchecked")public final V get() {InternalThreadLocalMap threadLocalMap = InternalThreadLocalMap.get();Object v = threadLocalMap.indexedVariable(index);if (v != InternalThreadLocalMap.UNSET) {return (V) v;}// 值为 UNSET,触发懒初始化return initialize(threadLocalMap);}
initialize() 调用 initialValue() 获取初始值(子类可重写,默认返回 null),然后写入数组并注册到 VARIABLES_TO_REMOVE_INDEX。注意 initialValue() 不能返回 UNSET,否则会抛出 IllegalArgumentException:
io.netty.util.concurrent.FastThreadLocal#initializeprivate V initialize(InternalThreadLocalMap threadLocalMap) {V v = null;try {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);addToVariablesToRemove(threadLocalMap, this);return v;}
FastThreadLocal 还提供了两个变体方法。getIfExists() 使用 getIfSet() 而非 get(),线程无 Map 时直接返回 null,不触发初始化:
io.netty.util.concurrent.FastThreadLocal#getIfExists@SuppressWarnings("unchecked")public final V getIfExists() {InternalThreadLocalMap threadLocalMap = InternalThreadLocalMap.getIfSet();if (threadLocalMap != null) {Object v = threadLocalMap.indexedVariable(index);if (v != InternalThreadLocalMap.UNSET) {return (V) v;}}return null;}
另一个重载版本 get(InternalThreadLocalMap) 允许调用方复用已获取的 Map 引用,避免重复调用 Thread.currentThread() 的开销。这在循环中高频调用 FastThreadLocal 时特别有用:
io.netty.util.concurrent.FastThreadLocal#get(InternalThreadLocalMap)@SuppressWarnings("unchecked")public final V get(InternalThreadLocalMap threadLocalMap) {Object v = threadLocalMap.indexedVariable(index);if (v != InternalThreadLocalMap.UNSET) {return (V) v;}return initialize(threadLocalMap);}
五、remove() 与内存泄漏防护
FastThreadLocal#remove() 将当前线程的变量值重置为 UNSET,之后任何 get() 调用都会重新触发 initialValue()。入口调用 remove(InternalThreadLocalMap.getIfSet()),以无 Map 时为 null 安全处理:
io.netty.util.concurrent.FastThreadLocal#removepublic final voidremove() {remove(InternalThreadLocalMap.getIfSet());}@SuppressWarnings("unchecked")public final voidremove(InternalThreadLocalMap threadLocalMap) {removeAndGet(threadLocalMap);}
removeAndGet() 是删除的核心逻辑。它调用 threadLocalMap.removeIndexedVariable(index) 将数组槽位重置为 UNSET,然后从 VARIABLES_TO_REMOVE_INDEX 集合中移除当前 FastThreadLocal,最后触发 onRemoval(value) 回调(子类可重写用于释放资源):
io.netty.util.concurrent.FastThreadLocal#removeAndGet@SuppressWarnings("unchecked")private V removeAndGet(InternalThreadLocalMap threadLocalMap) {if (threadLocalMap == null) {return null;}Object v = threadLocalMap.removeIndexedVariable(index);if (v != InternalThreadLocalMap.UNSET) {// 从 VARIABLES_TO_REMOVE_INDEX 集合中移除,防止重复清理removeFromVariablesToRemove(threadLocalMap, this);try {// 回调通知子类释放资源onRemoval((V) v);} catch (Exception e) {PlatformDependent.throwException(e);}return (V) v;}return null;}
removeAll() 是 FastThreadLocal 的内存泄漏防护核心。它是一个静态方法,从 VARIABLES_TO_REMOVE_INDEX 位置取出所有已注册的 FastThreadLocal 实例,逐一调用 remove(),最后通过 InternalThreadLocalMap.remove() 清空整个 Map:
io.netty.util.concurrent.FastThreadLocal#removeAllpublic static void removeAll() {InternalThreadLocalMap threadLocalMap = InternalThreadLocalMap.getIfSet();if (threadLocalMap == null) {return;}try {Object v = threadLocalMap.indexedVariable(VARIABLES_TO_REMOVE_INDEX);if (v != null && v != InternalThreadLocalMap.UNSET) {@SuppressWarnings("unchecked")Set<FastThreadLocal<?>> variablesToRemove = (Set<FastThreadLocal<?>>) v;FastThreadLocal<?>[] variablesToRemoveArray =variablesToRemove.toArray(new FastThreadLocal[0]);for (FastThreadLocal<?> tlv: variablesToRemoveArray) {tlv.remove(threadLocalMap);}}} finally {InternalThreadLocalMap.remove();}}
内存泄漏防护的完整机制是这样的:每个 FastThreadLocal 变量在首次 set() 时通过 addToVariablesToRemove() 注册到 VARIABLES_TO_REMOVE_INDEX 位置(该位置存储一个 Set<FastThreadLocal<?>> 集合)。VARIABLES_TO_REMOVE_INDEX 本身也通过 nextVariableIndex() 静态字段初始化分配,由于是类加载时首次调用 nextVariableIndex(),始终为 0。当 FastThreadLocalRunnable 或 FastThreadLocalThread 的线程执行完毕时,通过 removeAll() 一次性清理所有注册的变量,确保线程池场景下的内存安全。
六、FastThreadLocalThread:线程绑定与快速路径
FastThreadLocalThread 继承自 Thread,核心字段是 InternalThreadLocalMap threadLocalMap。这个字段直接注入在 Thread 对象上,使得 InternalThreadLocalMap.get() 可以通过 instanceof 判断直接读取该字段,无需经过 JDK ThreadLocal 的哈希表查找:
io.netty.util.concurrent.FastThreadLocalThreadpublic class FastThreadLocalThread extends Thread {private InternalThreadLocalMap threadLocalMap;public FastThreadLocalThread() {cleanupFastThreadLocals = false;}public FastThreadLocalThread(Runnable target) {// 通过 FastThreadLocalRunnable.wrap() 包装 Runnablesuper(FastThreadLocalRunnable.wrap(target));cleanupFastThreadLocals = true;}
InternalThreadLocalMap.get() 是快速路径与慢速路径的分发入口。它通过 Thread.currentThread() instanceof FastThreadLocalThread 判断,分别走 fastGet() 或 slowGet():
io.netty.util.internal.InternalThreadLocalMap#getpublicstatic InternalThreadLocalMap get(){Thread thread = Thread.currentThread();if (thread instanceof FastThreadLocalThread) {return fastGet((FastThreadLocalThread) thread);} else {return slowGet();}}
快速路径 fastGet() 直接读取 thread.threadLocalMap 字段,若为 null 则创建新的 InternalThreadLocalMap 并注入:
io.netty.util.internal.InternalThreadLocalMap#fastGetprivatestatic InternalThreadLocalMap fastGet(FastThreadLocalThread thread) {InternalThreadLocalMap threadLocalMap = thread.threadLocalMap();if (threadLocalMap == null) {thread.setThreadLocalMap(threadLocalMap = new InternalThreadLocalMap());}return threadLocalMap;}
慢速路径 slowGet() 则回退到 JDK ThreadLocal 兜底。InternalThreadLocalMap 内部维护了一个 ThreadLocal<InternalThreadLocalMap> slowThreadLocalMap 静态字段,对普通线程走标准的 JDK ThreadLocal 路径:
io.netty.util.internal.InternalThreadLocalMap#slowGetprivatestatic InternalThreadLocalMap slowGet() {InternalThreadLocalMap ret = slowThreadLocalMap.get();if (ret == null) {ret = new InternalThreadLocalMap();slowThreadLocalMap.set(ret);}return ret;}

FastThreadLocalRunnable 是保证线程池场景下内存安全的关键组件。它的 run() 方法在 finally 块中调用 FastThreadLocal.removeAll(),确保线程执行完毕后所有 FastThreadLocal 变量被清理:
io.netty.util.concurrent.FastThreadLocalRunnablefinal class FastThreadLocalRunnable implements Runnable {private final Runnable runnable;private FastThreadLocalRunnable(Runnable runnable) {this.runnable = ObjectUtil.checkNotNull(runnable, "runnable");}@Overridepublic void run() {try {runnable.run();} finally {// 任务执行完毕后统一清理所有 FastThreadLocal 变量FastThreadLocal.removeAll();}}static Runnable wrap(Runnable runnable) {return runnable instanceof FastThreadLocalRunnable ? runnable: new FastThreadLocalRunnable(runnable);}}
FastThreadLocalThread 的构造器在传入 Runnable 时自动调用 FastThreadLocalRunnable.wrap(target) 包装,并设置 cleanupFastThreadLocals = true。这意味着所有通过 FastThreadLocalThread 执行的 Runnable 都会在结束时自动清理。
虚拟线程兼容是 4.2.x 的重要特性。runWithFastThreadLocal(Runnable) 通过 FallbackThreadSet(基于 LongLongHashMap 的线程 ID 集合)标记虚拟线程,使其获得 FastThreadLocal 支持,执行完毕后自动清理:
io.netty.util.concurrent.FastThreadLocalThread#runWithFastThreadLocalpublicstaticvoidrunWithFastThreadLocal(Runnable runnable) {Thread current = currentThread();if (current instanceof FastThreadLocalThread) {throw new IllegalStateException("Caller is a real FastThreadLocalThread");}long id = current.getId();fallbackThreads.updateAndGet(set -> {if (set.contains(id)) {throw new IllegalStateException("Reentrant call to run()");}return set.add(id);});try {runnable.run();} finally {fallbackThreads.getAndUpdate(set -> set.remove(id));FastThreadLocal.removeAll();}}
FallbackThreadSet 使用位图压缩存储线程 ID,通过 LongLongHashMap 实现,每个线程 ID 仅占用 1 个 bit,内存开销极小。contains() 通过 threadId >>> 6 定位到 64 位 long 所在的桶,1L << (threadId & 63) 定位到该 long 中的具体位,实现了 O(1) 的线程 ID 查找。
七、整体链路串联
将以上各小节串联起来,FastThreadLocal 的完整生命周期链路如下:
构造阶段:
new FastThreadLocal()调用InternalThreadLocalMap.nextVariableIndex(),通过全局AtomicInteger获取唯一递增索引,存储在this.index。首次写入:
set(value)→getAndSet(value)→InternalThreadLocalMap.get()获取线程 Map →setKnownNotUnset()写入数组 →addToVariablesToRemove()注册到清理集合。读取:
get()→InternalThreadLocalMap.get()→indexedVariable(index)数组直接读取。若为UNSET则触发initialize()懒初始化。清理:
remove()或removeAll()→removeIndexedVariable(index)重置为UNSET→removeFromVariablesToRemove()从清理集合注销 →onRemoval()回调。FastThreadLocal的设计思想可以凝练为:用空间换时间,用编译期常量索引换运行期哈希计算。 每个FastThreadLocal实例在构造时确定的index是一个编译期常量(对于 JVM 运行时而言),数组访问在 CPU 层面仅需一次地址偏移计算,远快于ThreadLocalMap的哈希计算 + 线性探测。InternalThreadLocalMap的价值不仅在于为FastThreadLocal提供数组存储,它还是一个通用的"线程本地存储"容器。除了indexedVariables数组,它还承载了stringBuilder(线程级StringBuilder复用)、charsetEncoderCache/charsetDecoderCache(编码器缓存)、arrayList(线程级ArrayList复用)、handlerSharableCache(Handler 共享性缓存)等 Netty 内部常用对象的线程级复用,最大限度减少了对象创建和 GC 压力。与 JDK
ThreadLocal的对比总结如下:
全文小结
本文聚焦 Netty FastThreadLocal 的源码实现,从存储结构与访问路径两个维度,深入分析了其用数组索引替代哈希表、O(1) 无冲突访问的设计原理。
在存储结构方面,InternalThreadLocalMap 通过 Object[] indexedVariables 稀疏数组 + UNSET 哨兵,配合 AtomicInteger 全局递增索引分配,实现了构造期确定的常量位置访问。在访问路径方面,FastThreadLocalThread 通过字段直接注入 InternalThreadLocalMap,避免了 ThreadLocalMap 的哈希计算开销;非 FastThreadLocalThread 场景则回退到 JDK ThreadLocal 兜底。FastThreadLocalRunnable 通过 finally 块中的 removeAll() 保证了线程池场景下的内存安全,FastThreadLocalThread.runWithFastThreadLocal() 为虚拟线程提供了兼容支持。
原创不易,如果本文对您有帮助,带来了些许灵感或启发,烦请动动小手点赞、关注、转发、收藏。这是作者持续更新的动力源泉,衷心感谢您的支持。我会尽量在工作之余,为大家带来更高品质的内容,努力保持周更。