fastthreadlocal 通过预分配全局唯一数组下标替代哈希表查找,使 get/set 变为直接数组访问;需继承 fastthreadlocalthread、手动清理、重写 initialize(),且不支持弱引用自动回收。

FastThreadLocal 为什么能绕过哈希表查找
普通 ThreadLocal 在每个线程内部维护一个 ThreadLocalMap,本质是用开放地址法实现的哈希表。每次 get() 或 set() 都要计算 hash、探测冲突、处理脏 entry——哪怕只有几个变量,也得走完整套逻辑。
FastThreadLocal 把这事砍掉了:它不查表,直接用预分配的数组下标定位。每个 FastThreadLocal 实例在创建时就拿到一个全局唯一的 index(类型是 int),线程持有的 FastThreadLocalThread 里有个 Object[] 数组,get() 就是 array[index],set() 就是 array[index] = value。
- 这个
index是静态递增分配的,首次使用时由InternalThreadLocalMap.nextVariableIndex()返回并缓存,后续复用 - 数组长度按需扩容,但扩容后旧数据会拷贝,不是无限增长;默认初始容量是 32,最大支持约 1024 个不同
FastThreadLocal - 不支持弱引用自动清理,所以必须手动调用
FastThreadLocal.removeAll(),否则可能内存泄漏
必须继承 FastThreadLocalThread 才能生效
FastThreadLocal 不是“换个类名就能提速”,它依赖线程对象里有 InternalThreadLocalMap 字段。普通 Thread 没这东西,强行用 FastThreadLocal.get() 会触发 fallback 到慢路径(即退化为标准 ThreadLocal 行为)。
- Netty 的
EventLoop内部创建的是FastThreadLocalThread,所以 IO 线程天然支持 - 你自己起的线程如果要用
FastThreadLocal,得显式 newFastThreadLocalThread(runnable),不能用new Thread(runnable) - Spring 管理的线程池(如
ThreadPoolTaskExecutor)默认用Thread,需自定义ThreadFactory返回FastThreadLocalThread
get() 和 set() 的空值处理差异
普通 ThreadLocal 的 get() 如果没 set() 过,会调用 initialValue() 并缓存结果;而 FastThreadLocal 的 get() 默认返回 null,除非你重写了 initialize() 方法且在线程中显式触发了初始化逻辑。
- 常见错误:以为
FastThreadLocal也会自动调用initialValue(),结果拿到null后 NPE - 正确做法是重写
initialize(FastThreadLocalThread thread),并在其中返回默认值,或在业务代码里先判空再初始化 -
set(null)是合法的,但之后get()仍返回null,无法区分“没设过”和“设过 null”,这点比标准ThreadLocal更不友好
数组下标优化在高并发下的真实收益
单次操作快不等于整体快。真正拉开差距的场景是:线程频繁访问多个 FastThreadLocal 变量(比如 Netty 的 ByteBufAllocator、ChannelHandler 上下文、解码器状态等),且这些变量集中在同一个线程生命周期内密集读写。
- 哈希表查找在 key 少时看似快,但 JVM 无法完全消除边界检查和分支预测失败开销;数组下标访问可被 JIT 更激进地优化成纯内存偏移
- 实测在百万级
get()循环中,FastThreadLocal比ThreadLocal快 2–3 倍,但前提是线程类型正确、索引已分配、无扩容抖动 - 注意副作用:所有
FastThreadLocal共享同一数组,如果某个业务大量申请新实例(比如动态生成 handler),可能导致数组频繁扩容,反而拖慢其他变量访问
真正在意性能时,别只盯着 get/set 单次耗时,得看整个线程的 local 变量布局是否紧凑、是否及时清理、是否误用了非 FastThreadLocalThread。










