ThreadLocal线程上下文

ThreadLocal是Java中用于线程局部变量的工具类,避免多线程竞争,常见于全链路跟踪、多租户场景。每个线程有自己的ThreadLocalMap,以弱引用存储ThreadLocal对象,防止内存泄漏。然而,不恰当的使用可能导致内存泄漏。InheritableThreadLocal允许子线程继承变量,但不适合线程池。TransmittableThreadLocal解决了这个问题,提供跨线程数据传递的能力。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

ThreadLocal

ThreadLocal 本地线程变量,主要用于解决数据访问的竞争,通常用于多租户、全链路压测、链路跟踪中保存 线程上下文环境,在一个请求流转中非常方便的获取一些关键信息,例如当前的租户信息、压测标记。


ThreadLocal 正如其名,本地线程变量,即数据存储在线程自己的局部变量中


其整体架构如下图所示:

ppp

ThreadLocal的核心设计理念总结如下:


每一个线程对象会维护一个私有属性:ThreadLocal.ThreadLocalMap threadLocals


ThreadLocalMap 内部结构为Key-Value键值对,其Key为ThreadLocal对象,Value为调用ThreadLocal的set方法设置的值。


一言以蔽之:ThreadLocal是将线程需要访问的数据存储在线程对象自身中,从而避免多线程的竞争。

Thread类

/* ThreadLocal values pertaining to this thread. This map is maintained
 * by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;

ThreadLocal 及 ThreadLocal.ThreadLocalMap

public class ThreadLocal<T> {

    /**
     * ThreadLocalMap is a customized hash map suitable only for
     * maintaining thread local values. No operations are exported
     * outside of the ThreadLocal class. The class is package private to
     * allow declaration of fields in class Thread.  To help deal with
     * very large and long-lived usages, the hash table entries use
     * WeakReferences for keys. However, since reference queues are not
     * used, stale entries are guaranteed to be removed only when
     * the table starts running out of space.
     */
    static class ThreadLocalMap {

        /**
         * 自定制的hash map entry,不同于hashmap的Node<K,V>,这里的节点key是ThreadLocal类型,而value可以指定类型
         * 思考:为什么使用弱引用类型key?【重要】
         */
        static class Entry extends WeakReference<ThreadLocal<?>> {		//弱引用
            /** The value associated with this ThreadLocal. */
            Object value;

            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }
    }
    
    public T get() {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);//ThreadLocal.ThreadLocalMap threadLocals
        if (map != null) {
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        return setInitialValue();
    }

    public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }
    
     public void remove() {
         ThreadLocalMap m = getMap(Thread.currentThread());
         if (m != null)
             m.remove(this);
     }
    
}

简单使用:

public class TLDemo {
    public static void main(String[] args) {

        for (int i = 0; i < 10; i++) {

            int finalI = i;
            new Thread(() -> {

                //放进threadlocalmap
                UserContext.set(new UserInfo(finalI + "", null, null));

                try { TimeUnit.SECONDS.sleep(1); } catch (Exception e) { }

                //从threadlocalmap中取
                UserInfo info1 = UserContext.get();
                System.out.println(Thread.currentThread().getName() + " " + info1);

            }, i + "").start();

        }

    }
}


class UserContext {
    private static final ThreadLocal<UserInfo> userContext = new ThreadLocal<>();

    public static UserInfo get() {
        return userContext.get();
    }

    public static void set(UserInfo ui) {
        userContext.set(ui);
    }

}

ThreadLocal为何被设计为弱引用?

Map 中的用于存储键值对的 Entry 为什么要继承 WeakReference?

思考这个问题之前先和大家普及一下 Java 的 4 种引用类型,主要是在垃圾回收时 java 虚拟机会根据不同的引用类型采取不同的措施

  • 强引用:java默认的引用类型,例如 Object a = new Object();其中 a 为强引用,new Object() 为一个具体的对象。一个对象从根路径能找到强引用指向它,jvm 虚拟机就不会回收。
  • 软引用(SoftReference):进行年轻代的垃圾回收不会触发 SoftReference 所指向对象的回收;但如果触发 Full GC,那 SoftReference 所指向的对象将被回收。备注:是除了软引用之外没有其他强引用引用的情况下
  • 弱引用(WeakReference) :如果对象除了有弱引用指向它后没有其他强引用关联它,当进行年轻代垃圾回收时,该引用指向的对象就会被垃圾回收器回收
  • 虚引用(PhantomeReference) 该引用指向的对象,无法对垃圾收集器收集对象时产生任何影响,但在执行垃圾回收后垃圾收集器会通过注册在 PhantomeReference 上的队列来通知应用程序对象被回收。

从四种弱引用的实际作用来说,主要是与垃圾回收器配合,决策什么时候可以将被引用的对象回收

在这里插入图片描述

根据第一部分,声明了一个 TheadLocal 对象,并且一个线程通过调用 threadLocal 对象的set(Object value) 存储了一个对象,其引用如上图所示。


ThreadLocal 的设计比较晦涩难懂,究其原因是 我们通过 threadLocal 对象的 set 方法进行存储值,但数据并不是存储在 ThreadLocal 对象中,而是存储在当前调用该方法的线程对象中。但从应用者的角度来看,我们操作的对象是ThreadLocal,从设计上来说就应该为它考虑。


试问一个问题:如果应用程序觉得 ThreadLocal 对象的使命完成,将 threadLocal ref 设置为null,如果 Entry 中引用 ThreadLocald 对象的引用类型设置为强引用的话,会发生什么问题?


答案是:ThreadLocal 对象会无法被垃圾回收器回收,因为从 thread 对象出发,有强引用指向 threadlocal obj。此时会违背用户的初衷,造成所谓的内存泄露


由于 ThreadLocalMap 中的 key 是指向 ThreadLocal,故从设计角度来看,设计为弱引用,将不会干扰用户释放 ThreadLocal 的意图

另一个问题:关于大量 Entry 造成的内存泄漏问题探讨

延展问题①:ThreadLocal 无法在父子线程之间传递数据

使用 InheritableThreadLocal

在这里插入图片描述

延展问题②:InheritableThreadLocal 的局限性

InheritableThreadLocal 支持子线程访问在父线程中设置的线程上下文环境的实现原理是在创建子线程时将父线程中的本地变量值复制到子线程,即复制的时机为创建子线程时


但我们提到并发、多线程就离不开线程池的使用,因为线程池能够复用线程,减少线程的频繁创建与销毁,如果使用 InheritableThreadLocal,那么线程池中的线程拷贝的数据来自于第一个提交任务的外部线程,即后面的外部线程向线程池中提交任务时,子线程访问的本地变量都来源于第一个外部线程,造成线程本地变量混乱

使用 TransmittableThreadLocal

参考文章:中间件兴趣圈

### 使用 `ThreadLocal` 存储线程上下文信息 为了确保每个线程拥有独立的上下文信息副本,可以利用 `ThreadLocal` 类实现这一点。下面是一个具体的例子展示如何通过 `ThreadLocal` 来保存并访问特定于当前执行线程的信息。 #### 创建 ThreadLocal 变量用于存储上下文数据 ```java public class ContextHolder { private static final ThreadLocal<String> contextHolder = new ThreadLocal<>(); public static String getContext() { return contextHolder.get(); } public static void setContext(String context) { contextHolder.set(context); } } ``` 这段代码定义了一个静态内部类 `ContextHolder` 和一个名为 `contextHolder` 的 `ThreadLocal` 成员变量[^2]。此成员变量用来保持字符串类型的值作为线程上下文的一部分。 #### 设置和获取线程上下文中的信息 当一个新的请求到来时,在处理该请求前设置好相应的上下文: ```java public class ServiceHandler implements Runnable { @Override public void run() { try { // 假设这是来自外部调用传入的一些参数 String requestParameter = "some-parameter"; // 将这些参数放入到当前线程上下文中去 ContextHolder.setContext(requestParameter); // 执行业务逻辑... processRequest(); // 完成后清除上下文以防内存泄漏 cleanup(); } catch (Exception e) { handleException(e); } } private void processRequest() { // 这里模拟实际工作中可能会做的工作 System.out.println("Processing with context: " + ContextHolder.getContext()); } private void cleanup() { // 移除不再需要的上下文信息以防止潜在的内存泄露问题 ContextHolder.contextHolder.remove(); } } ``` 在这个示例中,每当启动新的服务处理器实例(`ServiceHandler`)运行时,都会先将必要的输入参数存入到 `ContextHolder` 中;之后可以在任何地方轻松地读取这个上下文内的信息而不必担心跨线程污染的问题。最后一步非常重要——即清理掉已经完成的任务所关联的所有资源,从而避免不必要的对象滞留在堆空间内造成垃圾回收压力增大甚至引发 OOM 错误[^1]。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值