Thread Local的学习笔记

Thread Local的学习笔记

周四 5月 22 2025
2339 字 · 13 分钟
java 未分配标签

Thread Local的学习笔记

在多线程编程中,如何安全、简洁地为每个线程维护独立的变量副本,是开发者经常面临的挑战。Java 提供的 ThreadLocal 能够让我们轻松地在不同线程间隔离数据,避免了使用 synchronized 或复杂上下文传递的麻烦。本文将从使用方法、底层实现、常见陷阱与最佳实践几方面,深度剖析 ThreadLocal 的原理与应用,帮助你在日常开发中驾轻就熟地运用它。

一、Thread Local怎么使用?

  • 线程局部变量

    是一种用于实现线程局部变量的工具类。它允许每个线程都拥有自己的独立副本,从而实现线程隔离。每个线程各用各的

  • 怎么使用Thread Local?

    • 创建Thread Local

      JAVA
      //创建一个ThreadLocal变量
      public static ThreadLocal<String> localVariable = new ThreadLocal<>();
    • 设置Thread Local的值

      JAVA
      //设置ThreadLocal变量的值
      localVariable.set("天下万般兵刃,唯有过往伤人最深");
    • 获取Thread Local的值

      JAVA
      //获取ThreadLocal变量的值
      String value = localVariable.get();
    • 删除Thread Local的值

      JAVA
      //删除ThreadLocal变量的值
      localVariable.remove();
    • 记得用完要及时删除啊。至于原因,往下看你慢慢就知道了。

    OK,到了这里,Thread Local的基本使用你就已经学会了。往下看就是更深入的一些知识了。当然,个人水平有限,也是一个初学者,这个博客也只是记录了自己的一个学习过程。如有错误还请毫不留情的指出来。

    与普通的变量对比:

    MERMAID
    graph LR
        A[普通变量] --> B[多线程共享] --> C[需要同步控制]
        D[ThreadLocal变量] --> E[线程私有] --> F[无锁并发]

    每个线程访问的变量副本都是独立的,避免了共享变量引起的线程安全问题。由于 Thread Local 实现了变量的线程独占,使得变量不需要同步处理,因此能够避免资源竞争。

二、底层实现原理深度解析

  • Thread:每个线程持有ThreadLocal.ThreadLocalMap实例
  • Thread Local:提供get()/set()/remove()等操作方法
  • Thread Local Map:自定义哈希表,使用弱引用键(Entry extends Weak Reference)

每个 Thread 实例持有一个由 Thread Local 定义的 Thread Local Map 容器,Thread Local 通过自身实例作为键,在当前线程的 Thread Local Map 中存储 / 获取数据,实现线程间数据隔离。 上图:

image-20250511154550617

下面上一些Thread Local的源码看看

  • get方法

    JAVA
    /**
         * 返回当前线程对应的值;若不存在,则调用 setInitialValue 初始化。
         */
        public T get() {
            Thread t = Thread.currentThread();//这里的currentThread就是获取当前正在执行代码的线程对象的引用
            ThreadLocalMap map = getMap(t);//然后这里拿到这个线程的Thread Local Map
            if (map != null) {//判断Map是否存在(初始时线程的threadLocals字段为null,即Map未创建)
                ThreadLocalMap.Entry e = map.getEntry(this);//这里的this指向的是Thread Local实例对象,作为key去拿entry
                if (e != null) {
                    @SuppressWarnings("unchecked")
                    T result = (T) e.value;
                    return result;//拿到结果,return回去
                }
            }
            return setInitialValue();//这里是第一次,get的时候map为null走的逻辑,也就是初始化一下这个线程的map
        }
    private T setInitialValue() {
            T value = initialValue();//null//这里调用的就是下面的initialValue,拿到一个null
            Thread t = Thread.currentThread();//获取当前正在执行代码的线程对象的引用
            ThreadLocalMap map = getMap(t);//这里拿到这个线程的Thread Local Map 一开始确实是null哈
            if (map != null)
                map.set(this, value);// 若ThreadLocalMap已存在,将当前 ThreadLocal实例作为键this,初始值作为值,存入 map
            else
                createMap(t, value);//创建了一个map
            return value;
        }
    protected T initialValue() {
    return null;
    }
    	/**
         * 通过 Unsafe 在 Thread 对象上直接写入 threadLocals 字段
         */
        private void createMap(Thread t, T firstValue) {
            ThreadLocalMap m = new ThreadLocalMap(this, firstValue);
            UNSAFE.putObject(t, threadLocalsOffset, m);
        }
  • set方法 以Thread Local实例对象为key,value为值,去Thread Local map里面设值

    JAVA
    /**
         * 设置当前线程对应的值
         * @param value 要设置的值
         */
        public void set(T value) {
            Thread t = Thread.currentThread();
            ThreadLocalMap map = getMap(t);
            if (map != null)
                map.set(this, value);
            else
                createMap(t, value);
        }

接下来是Thread Local里面的静态类Thread Local Map的源码分析 🙂

Thread Local Map 中的每个 Entry 以弱引用包装 Thread Local 实例作为键,以强引用存储用户设置的值,弱引用键允许 Thread Local 实例在无强引用时被回收,结合主动清理机制避免内存泄漏。什么是强软弱虚引用后续再说

这张图很形象啊,图来自二哥 Thread → Thread Local Map → Entry(Key=Weak Ref, Value=强引用)

image-20250511164618347

JAVA
/* ———— 内部静态类:ThreadLocalMap ———— */
    /**
     * 每个线程持有一个 ThreadLocalMap,通过弱引用存储 ThreadLocal 键
     */
    static class ThreadLocalMap { //ThreadLocalMap里面装的是一个个Entry对象,key是弱引用,value是强引用
        /**
         * map 中的条目:弱引用 key + 强引用 value
         */
        static class Entry extends WeakReference<ThreadLocal<?>> {
            Object value;
            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }
        /** 初始容量,必须为 2 的幂 */
        private static final int INITIAL_CAPACITY = 16;
        /** 存储弱引用 Entry 的数组 */
        private Entry[] table;
        /** 当前键值对数量 */
        private int size      = 0;
        /** 重哈希阈值:当 size 超过 threshold 时扩容 */
        private int threshold;
        /**
         * 构造:首次插入
         * @param firstKey   ThreadLocal 实例
         * @param firstValue 对应值
         */
        ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
            table = new Entry[INITIAL_CAPACITY];
            threshold = INITIAL_CAPACITY * 2 / 3;
            int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
            table[i] = new Entry(firstKey, firstValue);
            size = 1;
        }
        /*----------------------比较重要的一些方法----------------------*/
        /**
         * 根据 ThreadLocal 实例(键)查找对应的 Entry(开放地址法 + 线性探测)
         */
        private Entry getEntry(ThreadLocal<?> key) {
            Entry[] tab = table;
            int len = tab.length;
            // 计算初始哈希索引(利用 threadLocalHashCode 保证不同实例哈希值均匀分布)
            int i = key.threadLocalHashCode & (len - 1);
            Entry e = tab[i];
            while (e != null && e.get() != key) {
                i = nextIndex(i, len);
                e = tab[i];
            }
            return e;
        }

        /**
         * 插入或更新键值对,同时清理过期弱引用条目并处理扩容。
         *aram key ThreadLocal 实例(键)
     	 * @param value 用户存储的值(强引用)
		*这里的set方法就是上面的public set方法中最终调用的set方法
         */
        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;
                }
                if (k == null) {
                    // 旧弱引用:清理并重用槽位
                    replaceStaleEntry(key, value, i);
                    return;
                }
            }
            // 未找到,插入新 Entry
            tab[i] = new Entry(key, value);
            int sz = ++size;
            if (!cleanSomeSlots(i, sz) && sz >= threshold)
                rehash();
        }

三、Thread Local的内存泄露怎么回事?

​ 通过前面的阅读,我们可以知道:Thread Local Map 的 Key是弱引用,但Value是强引用。 ​ Thread Local Map使用Thread Local的弱引用作为key,如果一个Thread Local没有外部强引用引用它,那么系统GC的时候,这个Thread Local势必会被回收,这样一来,Thread local Map中就会出现key为null的Entry,既然key为null,那你就访问不到了,如果当前线程迟迟不能结束(比如你正好用的线程池)这些key为null的Entry的value就会一直存在一条强引用。

image-20250511171326742 怎么解决呢?

​ 使用完 Thread Local 后,及时调用 remove() 方法释放内存空间。

JAVA
try {
    threadLocal.set(value);
    // 执行业务操作
} finally {
    threadLocal.remove(); // 确保能够执行清理
}

remove() 方法会将当前线程的 Thread Local Map 中的所有 key 为 null 的 Entry 全部清除,这样就能避免内存泄漏问题。

JAVA
private void remove(ThreadLocal<?> key) {
    Entry[] tab = table;
    int len = tab.length;
    // 计算 key 的 hash 值
    int i = key.threadLocalHashCode & (len-1);
    // 遍历数组,找到 key 为 null 的 Entry
    for (Entry e = tab[i];
            e != null;
            e = tab[i = nextIndex(i, len)]) {
        if (e.get() == key) {
            // 将 key 为 null 的 Entry 清除
            e.clear();
            expungeStaleEntry(i);
            return;
        }
    }
}

public void clear() {
    this.referent = null;
}

Thread Local Map的扩容机制

  • 扩容触发条件:
    • ThreadLocalMap中,当size(当前有效键值对数量)超过threshold(阈值)时,会触发扩容。threshold的初始值为INITIAL_CAPACITY(默认 16)的 2/3,即threshold = INITIAL_CAPACITY * 2 / 3
  • 扩容过程:
    • 创建新数组:扩容时,会创建一个新的Entry数组,新数组的长度是原来的 2 倍。例如,原来的容量是 16,扩容后变为 32。
    • 重新计算索引并迁移数据:遍历旧数组中的每个Entry,对于每个非空的Entry,重新计算其在新数组中的索引位置。计算索引的方式是使用ThreadLocal实例的threadLocalHashCode与新数组长度减 1 进行按位与操作(h = k.threadLocalHashCode & (newLen - 1))。如果新位置已经被占用,则通过线性探测(while (newTab[h] != null) h = nextIndex(h, newLen);)找到下一个可用的位置,然后将Entry放入新数组。
    • 更新相关参数:扩容后,threshold会更新为新容量的 2/3,table会指向新的数组。
  • 扩容的意义:扩容可以减少哈希冲突,提高ThreadLocalMap的查找效率。当哈希表中的元素越来越多,哈希冲突的概率会增加,通过扩容可以重新分配元素的存储位置,使元素分布更加均匀。

四、Thread Local的应用场景

JAVA
// Web 应用中为每个请求绑定用户上下文
private static final ThreadLocal<UserContext> userContext = ThreadLocal.withInitial(UserContext::new);

public void handleRequest(Request req) {
    try {
        userContext.get().setUserId(req.getParameter("userId"));
        // 业务逻辑
    } finally {
        userContext.remove();
    }
}

五、来点面试题


Thanks for reading!

Thread Local的学习笔记

周四 5月 22 2025
2339 字 · 13 分钟
java 未分配标签

© TguoV | CC BY-NC-SA 4.0