ThreadLocal详解

发布于:2025-07-15 ⋅ 阅读:(13) ⋅ 点赞:(0)


在多线程编程中,我们创建的变量默认可以被任何线程访问和修改。这无疑带来了数据竞争和线程安全的挑战。那么,如果想让每个线程都拥有自己的专属变量,互不干扰,该如何实现呢?

答案就是 JDK 提供的 ThreadLocal

一、ThreadLocal 是什么?为什么需要它?

ThreadLocal 类允许我们创建只能被当前线程读写的变量。可以将其形象地比喻为一个“存放私有物品的盒子”。每个线程进入系统时,都会得到一个属于自己的、独立的盒子。线程可以随时向自己的盒子里存放或取用物品,但无法窥探或操作其他线程的盒子。

当你创建一个 ThreadLocal 变量时,每个访问该变量的线程都会获得一个独立的、初始化的副本。这也是 ThreadLocal 名称的由来。线程可以通过 get() 方法获取自己线程的本地副本,或通过 set() 方法修改该副本的值,从而根除了线程安全问题。

举个简单的例子:假设有两个人去宝屋寻宝。如果他们共用一个袋子,必然会产生争执;但如果每个人都有一个独立的袋子,就不会有这个问题。如果将这两个人比作线程,那么 ThreadLocal 就是为他们提供独立袋子的方法。

代码示例:

public class ThreadLocalExample {
    // 使用 withInitial 为每个线程提供一个初始值为 0 的副本
    private static ThreadLocal<Integer> threadLocal = ThreadLocal.withInitial(() -> 0);

    public static void main(String[] args) {
        Runnable task = () -> {
            // 1. 获取当前线程的副本值
            int value = threadLocal.get();
            // 2. 在副本上进行修改
            value += 1;
            // 3. 将修改后的值设置回当前线程的副本
            threadLocal.set(value);
            // 4. 再次获取并打印,验证隔离性
            System.out.println(Thread.currentThread().getName() + " Value: " + threadLocal.get());
        };

        Thread thread1 = new Thread(task, "Thread-1");
        Thread thread2 = new Thread(task, "Thread-2");

        thread1.start();
        thread2.start();
    }
}

输出:

Thread-1 Value: 1
Thread-2 Value: 1

可以看到,Thread-1Thread-2 各自对 threadLocal 的操作互不影响,都从初始值 0 变成了 1

二、ThreadLocal 究竟把数据存哪了?

要理解 ThreadLocal 的工作机制,我们从Thread 类的源码入手。

public class Thread implements Runnable {
    //...
    // 与此线程有关的 ThreadLocal 值。由 ThreadLocal 类维护
    ThreadLocal.ThreadLocalMap threadLocals = null;

    // 与此线程有关的 InheritableThreadLocal 值。由 InheritableThreadLocal 类维护
    ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
    //...
}

可以看出,每个 Thread 对象内部都包含一个名为 threadLocals 的成员变量,它的类型是 ThreadLocal.ThreadLocalMap

ThreadLocalMapThreadLocal 的一个静态内部类,可以理解为一个定制版的 HashMap。默认情况下 threadLocalsnull,只有当前线程首次调用 ThreadLocalset()get() 方法时,这个 Map 才会被创建。

set()方法调用过程

ThreadLocal.set(T value) 方法:

public void set(T value) {
    // 1. 获取发起调用的当前线程
    Thread t = Thread.currentThread();
    // 2. 从当前线程对象中,获取其内部的 threadLocals 变量(即那个 Map)
    ThreadLocalMap map = getMap(t);
    if (map != null)
        // 3a. 如果 Map 已存在,就将值存入 Map
        map.set(this, value);
    else
        // 3b. 如果 Map 不存在,就为该线程创建一个 Map 并存入初始值
        createMap(t, value);
}

// getMap(t) 的实现非常直接,就是返回线程的成员变量
ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
}

通过源码分析,我们得出结论:

  1. 数据并不存储在 ThreadLocal 实例中ThreadLocal 实例扮演的是一个“钥匙”或“入口”的角色。
  2. 数据真正存储在调用线程 Thread 自身的 threadLocals 字段(一个 ThreadLocalMap)中
  3. ThreadLocalMapkeyThreadLocal 实例本身,而 value 才是我们想要存储的数据。
一张图看懂三者关系

ThreadThreadLocalThreadLocalMap 之间的关系如下图所示:

在这里插入图片描述

每个线程(Thread)都有一个自己的 ThreadLocalMap。当我们在同一个线程中声明了多个 ThreadLocal 对象(如 userThreadLocaltraceIdThreadLocal),这些 ThreadLocal 实例会作为不同的 key,将它们各自的 value 存储在当前线程唯一的那个 ThreadLocalMap 中。

三、ThreadLocal 的内存泄漏陷阱

ThreadLocal 如果使用不当,会引发内存泄漏。这个问题的根源在于 ThreadLocalMap 的内部实现。

ThreadLocalMap 存储键值对的单位是其内部类 Entry

static class Entry extends WeakReference<ThreadLocal<?>> {
    /** The value associated with this ThreadLocal. */
    Object value;

    Entry(ThreadLocal<?> k, Object v) {
        super(k); // key 是一个弱引用
        value = v;  // value 是一个强引用
    }
}

这里的引用关系是导致内存泄漏的关键:

  • Key 是弱引用Entrykey 是一个 WeakReference,它包装了 ThreadLocal 实例。当一个对象只被弱引用指向时,垃圾回收器(GC)下一次运行时就会回收它。这意味着,如果外部代码不再有对 ThreadLocal 实例的强引用(比如 myThreadLocal = null),那么在下一次 GC 后,ThreadLocalMap 中对应 Entrykey 就会变成 null
  • Value 是强引用Entryvalue 是一个 Object 类型的强引用。即使 key 因为弱引用被回收了,Entry 对象本身还存在于 ThreadLocalMaptable 数组中,并且它依然强引用着我们存储的 value 对象。

内存泄漏就这样发生了:当一个 ThreadLocal 实例被回收(key 变为 null),但持有它的线程却一直存活(比如线程池中的复用线程),那么这个 keynullEntry 及其强引用的 value 将永远无法被回收,从而造成内存泄漏。

虽然 ThreadLocalMap 在执行 get()set()remove() 时会顺便清理一些 keynullEntry,但这种清理是被动的,不能保证及时性。

如何避免内存泄漏?

最有效、最推荐的做法是:在使用完 ThreadLocal 后,务必手动调用 remove() 方法。

remove() 方法会明确地将当前 ThreadLocal 对应的 Entry 从当前线程的 ThreadLocalMap 中移除,彻底切断引用链,避免内存泄漏。

特别是在线程池等线程会被复用的场景下,强烈建议使用 try-finally 结构来确保 remove() 的执行。

try {
    myThreadLocal.set(someValue);
    // ... 业务逻辑 ...
} finally {
    myThreadLocal.remove(); // 保证无论如何都会被执行
}

参考链接:https://javaguide.cn/java/concurrent/java-concurrent-questions-03.html#threadlocal-%E6%9C%89%E4%BB%80%E4%B9%88%E7%94%A8


网站公告

今日签到

点亮在社区的每一天
去签到