news 2026/8/5 20:56:31

一文看懂ThreadLocal的原理

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
一文看懂ThreadLocal的原理

先看下ThreadLocal的基本用法,创建5个线程给同一个ThreadLocal变量设置不同的值,从打印结果看每个线程设置和获取的值都是不同的,可见ThreadLocal为线程安全的,每个线程保存的值相互独立。

public class ThreadLocalTest { public static ThreadLocal local = new ThreadLocal(); public static void main(String[] args) { for (int i = 0; i < 5; i++){ new Thread(new Runnable() { @Override public void run() { local.set("hello" + Thread.currentThread()); System.out.println(local.get()); } }).start(); } } } //打印结果为: helloThread[Thread-4,5,main] helloThread[Thread-3,5,main] helloThread[Thread-2,5,main] helloThread[Thread-0,5,main] helloThread[Thread-1,5,main]

现在开始ThreadLocal的原理,过程比较复杂。首先看下ThreadLocal的set()方法存数据的过程,获取调用set方法的线程中持有的ThreadLocalMap(ThreadLocal.ThreadLocalMap threadLocals = null;),每个线程的ThreadLocalMap都是独立的,因此存储的值是不同的。

public void set(T value) { Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map != null) { map.set(this, value); } else { createMap(t, value); } } // 获取线程内置的ThreadLocalMap ThreadLocalMap getMap(Thread t) { return t.threadLocals; }

如果在一个线程中首次使用ThreadLocal保持数据,则需要创建ThreadLocalMap,ThreadLocalMap中保存数据的实体是Entry,保存数据的过程就是先计算这个ThreadLocal对象的hashcode,根据hashcode计算在Entry数组中的位置,然后将创建的Entry保存在这个位置。

void createMap(Thread t, T firstValue) { t.threadLocals = new ThreadLocalMap(this, firstValue); } ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) { table = new Entry[INITIAL_CAPACITY]; int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1); table[i] = new Entry(firstKey, firstValue); size = 1; setThreshold(INITIAL_CAPACITY); } // 弱引用 static class Entry extends WeakReference<ThreadLocal<?>> { /** The value associated with this ThreadLocal. */ Object value; Entry(ThreadLocal<?> k, Object v) { super(k); value = v; } }

调用set()设置值的时候,会根据ThreadLocal计算hashcode。ThreadLocal中的属性threadLocalHashCodeprivate final int threadLocalHashCode = nextHashCode();用来维护每个ThreadLocal的hash值。再根据hashcode计算Entry数组的索引,根据索引找到当前线程对应的Entry,这里分三种情况:

  1. 第一次设值,则直接添加如果是当前线程使用的ThreadLocalif (k == key),则将对象设置进来,即写到存储数据的Entry中
  2. ThreadLocalif (k == key)已经有值了,就直接更新
  3. ThreadLocal作为临时变量被gc了if (k == null),例如在方法代码块中声明的ThreadLocal临时变量在方法执行完时不存在了,Entry中的ThreadLocal作为key是弱引用就会被gc;或者线程销毁了,此时指向Entry的引用不存在了,Entry也会被gc,此时如果不gc的话就会出现一块无法访问到的Entry,造成内存泄漏。

set()时如果发现hash冲突,ThreadLocal的做法是向后移动一位,到数组的下个索引处保存Entry,如果下个索引处有值了再继续向后找。

private void set(ThreadLocal<?> key, Object value) { Entry[] tab = table; int len = tab.length; int i = key.threadLocalHashCode & (len-1); for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) { ThreadLocal<?> k = e.get(); // 更新 if (k == key) { e.value = value; return; } // 临时ThreadLocal被回收的处理 if (k == null) { replaceStaleEntry(key, value, i); return; } } // 第一次直接设置 tab[i] = new Entry(key, value); int sz = ++size; if (!cleanSomeSlots(i, sz) && sz >= threshold) rehash(); } // 向后移动一位 private static int nextIndex(int i, int len) { return ((i + 1 < len) ? i + 1 : 0); }

当通过get()方法获取数据时,首先找到当前的线程对象,获取线程对象内部的ThreadLocalMap,然后根据ThreadLocal对象计算Entry的索引,找到本线程存储数据的Entry,获取Entry中的数据。

public T get() { Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map != null) { ThreadLocalMap.Entry e = map.getEntry(this); if (e != null) { @SuppressWarnings("unchecked") T result = (T)e.value; return result; } } return setInitialValue(); } private Entry getEntry(ThreadLocal<?> key) { int i = key.threadLocalHashCode & (table.length - 1); Entry e = table[i]; if (e != null && e.get() == key) return e; else return getEntryAfterMiss(key, i, e); }
  • ThreadLocal内存泄漏的问题
    可以看到Entry是指向ThreadLocal的弱引用,弱引用不会阻止gc的垃圾回收,如果这个ThreadLocal对象置为null,指向ThreadLocal对象的弱引用不会阻止gc的垃圾回收,此时ThreadLocal对象会被gc回收,通过get()方法获取value时需要计算ThreadLocal对象的hashcode,在ThreadLocal对象被回收的情况就无法计算hashcode,也就无法访问这个value引用的对象,于是value就成了有引用链但是无法被访问的内存,即造成内存泄漏了。
static class Entry extends WeakReference<ThreadLocal<?>> { /** The value associated with this ThreadLocal. */ Object value; Entry(ThreadLocal<?> k, Object v) { super(k); value = v; } }

解决方法:

  1. 将ThreadLocal变量定义成private static,这样就一直存在ThreadLocal的强引用,可以通过ThreadLocal对象访问到保存的数据,不会造成内存泄漏
  2. 调用remove()方法清除内存
public void remove() { ThreadLocalMap m = getMap(Thread.currentThread()); if (m != null) { m.remove(this); } } private void remove(ThreadLocal<?> key) { Entry[] tab = table; int len = tab.length; int i = key.threadLocalHashCode & (len-1); for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) { if (e.get() == key) { e.clear(); expungeStaleEntry(i); return; } } }

总结一下,每个ThreadLocal都有每个线程对应的ThreadLocalMap用于保存数据,每个线程的ThreadLocalMap对象都不相同,所以是线程安全的。ThreadLocal存在内存泄漏问题,需要持有ThreadLocal的强引用或remove清理。有不对的地方请大神指出,欢迎大家一起讨论交流,共同进步,更多请关注微信公众号 葡萄开源。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/8/4 14:04:26

全网首发!万字硬核解析:如何用向量引擎API构建企业级GPT-5.2与Sora2双模态中台(附完整源码+架构图)

摘要本文不仅仅是一篇教程。 更是一次对当下AI开发模式的深度反思与重构。 随着GPT-5.2、Sora2、Veo3等核弹级模型的相继发布。 传统的“单点直连”开发模式已经彻底崩塌。 面对高昂的维护成本、复杂的网络环境以及碎片化的接口标准。 我们迫切需要一种新的架构思想。 本文将通…

作者头像 李华
网站建设 2026/8/4 14:05:27

冥想第一千七百七十七天(1777)

1.周三&#xff0c;今天还是特别忙&#xff0c;中午忙工作休息的时间特别短。天气比前两天好多了。 2.感谢父母&#xff0c;感谢朋友&#xff0c;感谢家人&#xff0c;感谢不断进步的自己。

作者头像 李华
网站建设 2026/7/31 15:19:46

状态观测器深入浅出:从“为何需要”到“如何设计”

今天补充一下观测器的理论知识。 引言:我们为何需要“猜测”系统内部? 在理想的控制系统设计中,我们通常假设所有需要的状态变量(比如位置、速度、温度、化学浓度等)都可以通过传感器直接、精确地测量。然而,现实很骨感,很多时候我们无法直接获取这些关键状态: 物理限…

作者头像 李华
网站建设 2026/7/24 16:00:31

Flutter 三端应用实战:OpenHarmony 简易文本首尾字符对比器开发指南

一、为什么需要“简易文本首尾字符对比器”&#xff1f; 在 OpenHarmony 的文本校验、格式验证与结构分析场景中&#xff0c;同时观察首尾字符能提供独特的上下文洞察&#xff1a; 程序员&#xff1a;检查字符串是否被正确包裹&#xff08;如 "..."、...、[...]、{..…

作者头像 李华
网站建设 2026/8/4 15:00:43

CMake工程指南(一):基础概念与核心优势

引言 在现代C/C开发中&#xff0c;构建系统是项目管理的核心。传统的构建方式往往面临跨平台兼容性差、配置复杂等问题。本教程将带你深入了解CMake——这个C/C开发的事实标准构建工具。 什么是构建系统&#xff1f; 构建&#xff08;Build&#xff09; 编译&#xff08;Compil…

作者头像 李华
网站建设 2026/8/4 15:01:30

【算法分享】R树索引

在车联网、POI 分析、地理围栏、行政区划判断等业务中&#xff0c;我们经常会遇到一个非常基础、但极其高频的问题&#xff1a; &#x1f449; 给定一个经纬度点&#xff0c;它属于哪个&#xff08;或哪些&#xff09;多边形&#xff1f; 比如&#xff1a; 一个 GPS 点属于哪个…

作者头像 李华