找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

819

积分

0

好友

113

主题
发表于 10 小时前 | 查看: 1| 回复: 0

ThreadLocal 是 Java 提供的一种线程局部变量机制,它为每个使用该变量的线程提供独立的副本,使得每个线程都可以独立地修改自己的副本而不会影响其他线程,从而巧妙避开了多线程环境下的数据竞争问题。

核心方法

方法 说明
T get() 返回当前线程所对应的线程局部变量的值。如果尚未设置,则调用 initialValue() 初始化并返回。
void set(T value) 将当前线程的线程局部变量设置为指定值。
void remove() 移除当前线程的线程局部变量值。在线程池等复用线程的场景中,必须显式调用 remove(),否则极易造成数据污染或内存泄漏。
protected T initialValue() 返回该线程局部变量的初始值。默认返回 null。通常通过匿名内部类或 Lambda 表达式重写此方法来提供默认值。

简单使用示例

public class ThreadLocalExample {

    private static final ThreadLocal<String> context = new ThreadLocal<>();

    public static void main(String[] args) throws InterruptedException {
        Runnable task = () -> {
            String threadName = Thread.currentThread().getName();

            // 设置线程局部变量
            context.set("User-" + threadName);
            System.out.println(threadName + " - 设置值: " + context.get());

            try {
                // 模拟业务逻辑(可能抛出异常)
                doSomething();
            } finally {
                // 关键:无论是否异常,都清除 ThreadLocal
                context.remove();
                System.out.println(threadName + " - 已清理 ThreadLocal");
            }

            // 验证是否已清除(应为 null)
            System.out.println(threadName + " - 清理后 get(): " + context.get());
        };

        Thread t1 = new Thread(task, "Thread-1");
        Thread t2 = new Thread(task, "Thread-2");

        t1.start();
        t2.start();

        t1.join();
        t2.join();

        System.out.println("Main thread - get(): " + context.get()); // null
    }

    private static void doSomething() {
        String user = context.get();
        if (user != null) {
            System.out.println("  [doSomething] 当前用户: " + user);
            // 模拟可能的异常
            // throw new RuntimeException("Oops!");
            // 模拟耗时工作
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
            }
        }
    }
}

输出:

Thread-1 - 设置值: User-Thread-1
  [doSomething] 当前用户: User-Thread-1
Thread-2 - 设置值: User-Thread-2
  [doSomething] 当前用户: User-Thread-2
Thread-1 - 已清理 ThreadLocal
Thread-1 - 清理后 get(): null
Thread-2 - 已清理 ThreadLocal
Thread-2 - 清理后 get(): null
Main thread - get(): null

这个示例清晰地展示了 ThreadLocal 在不同线程间的隔离性,以及 finally 块中调用 remove() 的重要性。

源码分析

JDK 8 ThreadLocal源码分析是理解其设计精髓的关键。其核心在于 ThreadThreadLocalThreadLocalMap 三者的协作:

// Thread 类:每个线程对象内部持有一个 ThreadLocalMap
public class Thread implements Runnable {
    /*
     * threadLocals 是当前线程私有的 ThreadLocalMap。
     * 它由 ThreadLocal 类负责维护(创建、读写),Thread 本身不操作它。
     * 初始值为 null,只有当线程第一次使用 ThreadLocal 时才会被初始化。
     */
    ThreadLocal.ThreadLocalMap threadLocals = null;
}

// ThreadLocal 类:为每个线程提供独立的变量副本
public class ThreadLocal<T> {
    /**
     * 获取当前线程中与该 ThreadLocal 实例关联的值。
     * 如果尚未设置,则调用 setInitialValue() 初始化。
     */
    public T get() {
        // 1. 获取当前正在执行的线程
        Thread t = Thread.currentThread();
        // 2. 从当前线程中获取其持有的 ThreadLocalMap(即 threadLocals 字段)
        ThreadLocal.ThreadLocalMap map = getMap(t);
        // 3. 如果 map 存在(即该线程至少使用过一个 ThreadLocal)
        if (map != null) {
            // 4. 以当前 ThreadLocal 实例(this)为 key,在 map 中查找对应的 Entry
            ThreadLocal.ThreadLocalMap.Entry e = map.getEntry(this);
            // 5. 如果找到了有效 Entry(未被回收且存在)
            if (e != null) {
                // 6. 取出 value 并强制转换为泛型类型 T(由于泛型擦除,需 unchecked 转换)
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        // 7. 如果 map 不存在 或 没有找到对应 entry,则进行初始化(通常返回 initialValue())
        return setInitialValue();
    }

    /**
     * 为当前线程设置该 ThreadLocal 的值。
     */
    public void set(T value) {
        // 1. 获取当前线程
        Thread t = Thread.currentThread();
        // 2. 尝试获取当前线程的 ThreadLocalMap
        ThreadLocal.ThreadLocalMap map = getMap(t);
        // 3. 如果 map 已存在(线程已使用过 ThreadLocal)
        if (map != null)
            // 4. 直接将 (this, value) 存入 map(this 作为 key)
            map.set(this, value);
        else
            // 5. 否则,首次使用,为该线程创建一个新的 ThreadLocalMap
            createMap(t, value);
    }

    /**
     * 从指定线程中获取其 ThreadLocalMap(即 threadLocals 字段)。
     * 这是 ThreadLocal 与 Thread 解耦的关键:ThreadLocal 不持有线程数据,而是从线程中“拉取”。
     */
    ThreadLocal.ThreadLocalMap getMap(Thread t) {
        return t.threadLocals; // 直接返回 Thread 对象的 threadLocals 成员
    }

    // 其他省略...
}

每个 Thread 对象内部都持有一个 ThreadLocal.ThreadLocalMap 类型的字段 threadLocals,初始为 null,首次使用 ThreadLocal 时才被初始化。这就是实现“线程隔离”的基石:不同线程操作的是各自独立的 map。

ThreadLocal 本身不存储值,它只是作为一个 key 访问当前线程的 map。调用 threadLocal.get() 时,实际上执行的是 Thread.currentThread().threadLocals.get(this)

关键的 ThreadLocalMap 与 Entry 设计

ThreadLocal.ThreadLocalMap 并非标准的 HashMap,而是 ThreadLocal 内部实现的一个专用哈希表。它采用 数组 + 开放寻址法(线性探测) 来解决哈希冲突。

// ThreadLocalMap:ThreadLocal 的内部静态类,一个定制化的哈希表,仅用于存储线程局部变量。
// 使用弱引用(WeakReference)作为 key(即 ThreadLocal 实例),防止内存泄漏。
static class ThreadLocalMap {
    /**
     * 哈希表底层数组,存储 Entry。
     */
    private Entry[] table;

    /**
     * Entry 继承 WeakReference<ThreadLocal<?>>,
     * 其 referent(父类字段)就是 ThreadLocal 实例(作为 key),而 value 是用户存储的对象。
     * 关键设计:
     * - key 是弱引用:当外部不再强引用某个 ThreadLocal 实例时,GC 可回收该 key。
     * - value 是强引用:若不手动 remove,即使 key 被回收,value 仍可能滞留,造成内存泄漏。
     */
    static class Entry extends WeakReference<ThreadLocal<?>> {
        /** 与该 ThreadLocal 关联的值 */
        Object value;

        Entry(ThreadLocal<?> k, Object v) {
            super(k);      // 将 k 作为弱引用的 referent
            value = v;     // 强引用 value
        }
    }

    // getEntry/set 等核心逻辑包括:
    // - 使用线性探测法解决哈希冲突
    // - 在 get/set 时会清理部分 stale entries(陈旧条目),
    //   这是 ThreadLocalMap 内部一种被动式内存回收机制,用于缓解因 ThreadLocal 实例被回收后,
    //   其对应的 value 仍残留在 ThreadLocalMap 中造成的内存泄漏问题。
    // 以 getEntry 方法为例:
    private Entry getEntry(ThreadLocal<?> key) {
        // 1. 计算 key 的哈希槽位索引
        int i = key.threadLocalHashCode & (table.length - 1);
        // 2. 获取该槽位上的 Entry
        Entry e = table[i];
        // 3. 检查是否“命中”
        if (e != null && e.get() == key)
            return e; // 直接命中,返回 Entry
        // 4. 如果未命中(可能是 null、key 不匹配、或 key 已被回收),进入线性探测查找
        else
            return getEntryAfterMiss(key, i, e);
    }
    // 当在直接哈希槽位未找到 key 时,使用线性探测继续查找。
    // 同时在此过程中清理遇到的 stale entry(key == null 的条目)。
    private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
        // 1. 获取当前 table 和长度(避免多次访问字段)
        Entry[] tab = table;
        int len = tab.length;

        // 2. 线性探测循环:只要当前 Entry 不为 null,就继续
        while (e != null) {
            // 3. 获取当前 Entry 的 key(即 WeakReference 指向的 ThreadLocal 实例)
            ThreadLocal<?> k = e.get();
            // 4. 情况一:找到目标 key
            if (k == key)
                return e; // 返回匹配的 Entry
            // 5. 情况二:发现 stale entry(key 已被 GC 回收,k == null)
            if (k == null)
                // 触发清理:从索引 i 开始,删除该 stale entry,
                // 并 rehash 后续受影响的 entry(防止断链)
                expungeStaleEntry(i);
            // 6. 情况三:key 存在但不匹配(哈希冲突),继续线性探测
            else
                i = nextIndex(i, len); // 计算下一个槽位((i + 1) % len)
            // 7. 移动到下一个槽位
            e = tab[i];
        }

        // 8. 遇到 null 槽位,说明 key 不存在(开放寻址法约定:插入位置在第一个 null 处)
        return null;
    }
}

这个设计有几个核心要点:

  1. 弱引用 KeyEntry 的 key(即 ThreadLocal 实例)是弱引用。当程序中没有其他地方强引用这个 ThreadLocal 对象时,GC 可以回收它,此时 Entry.get() 返回 null,该条目成为 stale entry(陈旧条目)。这避免了因为 ThreadLocalMap 的持有而导致 ThreadLocal 对象本身无法被回收。
  2. 强引用 ValueEntryvalue 是强引用。这带来了著名的内存泄漏风险:即使 ThreadLocal 实例被回收(key 变 null),只要线程还在运行(例如线程池中的核心线程),这个 value 就无法被 GC 回收,因为仍然有一条从当前线程 (Thread) -> ThreadLocalMap -> Entry -> value 的强引用链。
  3. 被动清理机制ThreadLocalMapgetEntryset 等操作进行线性探测时,会顺便清理路径上遇到的 stale entryexpungeStaleEntry)。这是一种被动且局部的防护,可以缓解但不能根治内存泄漏。它依赖于后续的哈希表操作恰好经过这些陈旧条目所在的槽位。

总结与最佳实践

ThreadLocal 的精妙之处在于将数据直接存储在 Thread 对象内部,实现了天然的线程隔离。但其 WeakReference key 与 StrongReference value 的组合是一把双刃剑,既防止了 key 的泄漏,又带来了 value 泄漏的风险。

因此,在多线程环境特别是使用线程池时,必须养成在 finally 块中显式调用 remove() 的习惯,这是杜绝数据错乱和内存泄漏最可靠的方式。仅仅依赖其内部的被动清理机制是远远不够的。

对于想深入探讨更多并发编程技巧和源码分析的朋友,可以到 云栈社区Java 板块 参与交流,那里有更多关于 JUC、虚拟线程等前沿话题的深度讨论。




上一篇:Hexo 插件自动同步追番数据:支持B站/Bangumi多平台展示页
下一篇:告别臃肿JSON:MessagePack等四种二进制协议性能实测与应用场景
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-1-27 18:16 , Processed in 0.311668 second(s), 40 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表