微服务之间共享threadlocal_threadlocal跨线程传递解决方案

本文详细介绍了ThreadLocal的概念及其应用场景,包括实现链路日志增强、自定义线程内缓存及同一线程下多个类间的数据传递。同时深入探讨了ThreadLocal的工作原理、内存泄露问题及其解决方案。

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

ThreadLocal简介

ThreadLocal 不是一个线程,而是一个线程的本地化对象。当某个变量在使用 ThreadLocal 进行维护时,ThreadLocal 为使用该变量的每个线程分配了一个独立的变量副本,每个线程可以自行操作自己对应的变量副本,而不会影响其他线程的变量副本。

  1. API 方法

ThreadLocal 的 API 提供了如下的 4 个方法。

1)protected T initialValue()

返回当前线程的局部变量副本的变量初始值。

2)T get()

返回当前线程的局部变量副本的变量值,如果此变量副本不存在,则通过 initialValue() 方法创建此副本并返回初始值。

3)void set(T value)

设置当前线程的局部变量副本的变量值为指定值。

4)void remove()

删除当前线程的局部变量副本的变量值。

threadlocal是做什么用的,用在哪些场景当中?

ThreadLocal 适用于每个线程需要自己独立的实例且该实例需要在多个方法中被使用,也即变量在线程间隔离而在方法或类间共享的场景。简单易懂的就是。每个线程有自己的数据副本,当线程结束后可以独立回收。

通常使用场景:

ThreadLocal+MDC实现链路日志增强

比如我们需要在整个链路的日志中输出当前登录的用户ID,首先就得在拦截器获取过滤器中获取用户ID,然后将用户ID进行存储到ThreadLocal。

然后再层层进行透传,如果用的Dubbo,那么就在Dubbo的Filter中进行传递到下一个服务中。问题来了,在Dubbo的Filter中如何获取前面存储的用户ID呢?

答案就是ThreadLocal。获取后添加到MDC中,就可以在日志中输出用户ID。

自定义线程内的缓存

同一次请求内,多次相同的查询获取 RPC 等的调用。
数据实时性要求高,不适合加缓存,主要是加缓存也不好设置过期时间,除非采用数据变更主动更新缓存的方式。
只需要在这一次请求里缓存即可,不影响其他地方。
不想改动已有代码。
总结后发现这个场景适合用 ThreadLocal 来传递数据,对已有代码改动量最小,而且也只对当前线程生效,不会影响其他线程。

ThreadLocal实现同一线程下多个类之间的数据传递

ThreadLocal 使用例子:

public class TestThreadLocal {
    //线程本地存储变量
    private static final ThreadLocal<Integer> THREAD_LOCAL_NUM = new ThreadLocal<Integer>() {
        @Override
        protected Integer initialValue() {
            return 0;
        }
        
    };
    public static void main(String[] args) {
        for (int i = 0; i < 3; i++) {//启动三个线程
            Thread t = new Thread() {
                @Override
                public void run() {
                    add10ByThreadLocal();
                }
            };
            t.start();
        }
    }

    /**
     * 线程本地存储变量加 5
     */
    private static void add10ByThreadLocal() {
        for (int i = 0; i < 5; i++) {
            Integer n = THREAD_LOCAL_NUM.get();
            n += 1;
            THREAD_LOCAL_NUM.set(n);
            System.out.println(Thread.currentThread().getName() + " : ThreadLocal num=" + n);
        }
        THREAD_LOCAL_NUM.remove();//手动清理,避免内存泄漏
    }

}

Thread-1 : ThreadLocal num=1Thread-2 : ThreadLocal num=1Thread-0 : ThreadLocal num=1Thread-2 : ThreadLocal num=2Thread-0 : ThreadLocal num=2Thread-2 : ThreadLocal num=3Thread-0 : ThreadLocal num=3Thread-2 : ThreadLocal num=4Thread-2 : ThreadLocal num=5Thread-0 : ThreadLocal num=4Thread-0 : ThreadLocal num=5Thread-1 : ThreadLocal num=2Thread-1 : ThreadLocal num=3Thread-1 : ThreadLocal num=4Thread-1 : ThreadLocal num=5

JDK 的实现里面这个 Map 是属于 Thread,而非属于 ThreadLocal。ThreadLocal 仅是一个代理工具类,内部并不持有任何与线程相关的数据,所有和线程相关的数据都存储在 Thread 里面。ThreadLocalMap 属于 Thread 也更加合理。

还有一个更加深层次的原因,这样设计不容易产生内存泄露。
ThreadLocal 持有的 Map 会持有 Thread 对象的引用,只要 ThreadLocal 对象存在,那么 Map 中的 Thread 对象就永远不会被回收。ThreadLocal 的生命周期往往比线程要长,所以这种设计方案很容易导致内存泄露。

JDK 的实现中 Thread 持有 ThreadLocalMap,而且 ThreadLocalMap 里对 ThreadLocal 的引用还是弱引用(WeakReference),所以只要 Thread 对象可以被回收,那么 ThreadLocalMap 就能被回收。JDK 的这种实现方案复杂但更安全。

原理

ThreadLocal在使用的时候是单独创建对象的,更像一个全局的容器。但是大家有没有想过一个问题,就是为啥要设计ThreadLocal这个类,而不使用HashMap这样的容器类?

ThreadLocal本质上是要解决线程之间数据的隔离,以达到互不影响的目的。如果我们用一个Map做数据存储,Key为线程ID, Value为你要存储的内容,其实也是能达到隔离的效果。

没错,效果是能达到,但是性能就不一定好了,涉及到多个线程进行数据操作。如果你不看ThreadLocal的源码,你肯定也会以为ThreadLocal就是这么实现的。

ThreadLocal在设计这块很巧妙,会在Thread类中嵌入一个ThreadLocalMap,ThreadLocalMap就是一个容器,用于存储数据的,但它在Thread类中,也就说存储的就是这个Thread类专享的数据。

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中的,key就是当前ThreadLocal的对象。
从使用的API看上去像是把值存储在了ThreadLocal中,其实值是存储在线程内部,然后关联了对应的ThreadLocal,这样通过ThreadLocal.get时就能获取到对应的值。

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();
}

在每个线程Thread内部有一个ThreadLocal.ThreadLocalMap类型的成员变量threadLocals,这个threadLocals就是用来存储实际的变量副本的,键值为当前ThreadLocal变量,value为变量副本(即T类型的变量)。 初始时,在Thread里面,threadLocals为空,当通过ThreadLocal变量调用get()方法或者set()方法,就会对Thread类中的threadLocals进行初始化,并且以当前ThreadLocal变量为键值,以ThreadLocal要保存的副本变量为value,存到threadLocals。 然后在当前线程里面,如果要使用副本变量,就可以通过get方法在threadLocals里面查找。

实际的通过ThreadLocal创建的副本是存储在每个线程自己的threadLocals中的;

为何threadLocals的类型ThreadLocalMap的键值为ThreadLocal对象,因为每个线程中可有多个threadLocal变量,就像上面代码中的longLocal和stringLocal;

在进行get之前,必须先set,否则会报空指针异常;如果想在get之前不需要调用set就能正常访问的话,必须重写initialValue()方法。 因为在上面的代码分析过程中,我们发现如果没有先set的话,即在map中查找不到对应的存储,则会通过调用setInitialValue方法返回i,而在setInitialValue方法中,有一个语句是T value = initialValue(), 而默认情况下,initialValue方法返回的是null。

在线程池中使用 ThreadLocal 为什么可能导致内存泄露呢?

在线程池中线程的存活时间太长,往往都是和程序同生共死的,这样 Thread 持有的 ThreadLocalMap 一直都不会被回收,再加上 ThreadLocalMap 中的 Entry 对 ThreadLocal 是弱引用(WeakReference),所以只要 ThreadLocal 结束了自己的生命周期是可以被回收掉的。
Entry 中的 Value 是被 Entry 强引用的,即便 value 的生命周期结束了,value 也是无法被回收的,导致内存泄露。

线程池中,如何正确使用 ThreadLocal?
在 finally 代码块中手动清理 ThreadLocal 中的 value,调用 ThreadLocal 的 remove()方法。

使用ThreadLocal有什么需要注意的吗?

避免跨线程异步传递
使用时记得及时remove, 防止内存泄露
注释说明使用场景,方便后人
对性能有极致要求可以参考开源框架的做法,用一些优化后的类,比如FastThreadLocal
比如Netty中就重写了一个FastThreadLocal来代替ThreadLocal,性能在一定场景下比ThreadLocal更好。

性能提升主要表现在如下几点:

FastThreadLocal操作数据的时候,会使用下标的方式在数组中进行查找来代替ThreadLocal通过哈希的方式进行查找。
FastThreadLocal利用字节填充来解决伪共享问题。
其实除了Netty中对ThreadLocal进行了优化,自定义了FastThreadLocal。在其他的框架中也有类似的优化,比如Dubbo中就InternalThreadLocal,根据源码中的注释,也是参考了FastThreadLocal的设计,基本上差不多。

如果我启动另外一个线程。那么在主线程设置的threadlocal值能被子线程拿到吗?

如果拿不到,怎么去解决这个问题

在java8中提供了一个这个类(InheritableThreadLocal):

从字面意思来看:可以实现父线程到子线程的共享

ThreadLocal有一个需求不能满足:就是子线程无法直接复用父线程的ThreadLocal变量里的内容。demo如下:

public class TestThreadLocal implements Runnable {
    private static ThreadLocal<String> threadLocal = new ThreadLocal<>();

    public static void main(String[] args) {
        System.out.println("----主线程设置值为\"主线程\"");
        threadLocal.set("主线程");
        System.out.println("----主线程设置后获取值:" + threadLocal.get());
        Thread tt = new Thread(new TestThreadLocal());
        tt.start();
        System.out.println("----主线程结束");

    }

    @Override
    public void run() {
        System.out.println("----子线程设置值前获取:" + threadLocal.get());
        System.out.println("----新开线程设置值为\"子线程\"");
        threadLocal.set("子线程");
        System.out.println("----新开的线程设置值后获取:" + threadLocal.get());
    }
}

可以看到虽然在main线程中启动了一个新的子线程,但是threadlocal变量的内容并没有传递到新的子线程中。

于是乎,InheritableThreadLocal就出现了。他可以实现在子线程中使用父线程中的线程本地变量(也即InheritableThreadLocal变量)。

InheritableThreadLocal用法


public class TestInheritableThreadLocal implements Runnable {
    private static InheritableThreadLocal<String> threadLocal = new InheritableThreadLocal<>();

    public static void main(String[] args) {
        System.out.println("----主线程设置值为\"主线程\"");
        threadLocal.set("主线程");
        System.out.println("----主线程设置后获取值:" + threadLocal.get());
        Thread tt = new Thread(new TestInheritableThreadLocal());
        tt.start();
        System.out.println("----主线程结束");

    }

    @Override
    public void run() {
        System.out.println("----子线程设置值前获取:" + threadLocal.get());
        System.out.println("----新开线程设置值为\"子线程\"");
        threadLocal.set("子线程");
        System.out.println("----新开的线程设置值后获取:" + threadLocal.get());
    }
}

在子线程设置值之前,就已经能够get到主线程设置的值了,说明在父子进制之间传递了InheritableThreadLocal变量。

InheritableThreadLocal原理:

复写了父类3个方法childValue,createMap,getMap。在当前线程上创建一个新的线程实例Thread时,会把这些线程变量从当前线程传递给新的线程实例。

public class InheritableThreadLocal<T> extends ThreadLocal<T> {
    /**
     * Computes the child's initial value for this inheritable thread-local
     * variable as a function of the parent's value at the time the child
     * thread is created.  This method is called from within the parent
     * thread before the child is started.
     * <p>
     * This method merely returns its input argument, and should be overridden
     * if a different behavior is desired.
     *
     * @param parentValue the parent thread's value
     * @return the child thread's initial value
     */
    protected T childValue(T parentValue) {
        return parentValue;
    }
    /**
     * Get the map associated with a ThreadLocal.
     *
     * @param t the current thread
     */
    ThreadLocalMap getMap(Thread t) {
       return t.inheritableThreadLocals;
    }
    /**
     * Create the map associated with a ThreadLocal.
     *
     * @param t the current thread
     * @param firstValue value for the initial entry of the table.
     */
    void createMap(Thread t, T firstValue) {
        t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);
    }
}

通过上面的代码我们可以看到InheritableThreadLocal 重写了childValue, getMap,createMap三个方法,当我们往里面set值的时候,值保存到了inheritableThreadLocals里面,而不是之前的threadLocals。

关键的点来了,为什么当创建新的线程时,可以获取到上个线程里的threadLocal中的值呢?原因就是在新创建线程的时候,会把之前线程的inheritableThreadLocals赋值给新线程的inheritableThreadLocals,通过这种方式实现了数据的传递。

源码最开始在Thread的init方法中,如下:

 if (parent.inheritableThreadLocals != null)
     this.inheritableThreadLocals =
                ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);

当主线程中对该变量进行set操作的时候,和ThreadLocal一样会初始化一个ThreadLocalMap对实际的变量值进行存储,以ThreadLocal为key,值为value,如果有多个ThreadLocal变量也都是存储在这个Map中。该Map使用的是HashMap的原理进行数据的存储,但是和ThreadLocal有一点差别,因为其覆写了createMap的方法。

在Thread类当中,可以看出Thread类维护了两个成员变量,ThreadLocal以及InheritableThreadLocal,数据类型都是ThreadLocalMap.这也就解释了为什么这个变量是线程私有的。但是如果要知道为什么父子线程的变量传递,那就继续看一下源码。当我们在主线程中开一个新的子线程的时候,开始会new一个新的Thread

在thread构造函数中,new一个thread方法
最后会调用ThreadLocal的createInheritedMap方法,而该方法会新建一个ThreadLocalMap,

parentMap就是父线程的ThreadLocalMap,这个构造函数的意思大概就是将父线程的ThreadLocalMap复制到自己的ThreadLocalMap里面来,这样我们就可以使用InheritableThreadLocal访问到父线程中的变量了

InheritableThreadLocal和线程池搭配使用的问题

首先给出结论:

InheritableThreadLocal和线程池搭配使用时,可能得不到想要的结果,因为线程池中的线程是复用的,并没有重新初始化线程,InheritableThreadLocal之所以起作用是因为在Thread类中最终会调用init()方法去把InheritableThreadLocal的map复制到子线程中。由于线程池复用了已有线程,所以没有调用init()方法这个过程,也就不能将父线程中的InheritableThreadLocal值传给子线程。

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class TestInheritableThreadLocalAndExecutor implements Runnable {
    private static InheritableThreadLocal<String> inheritableThreadLocal = new InheritableThreadLocal<>();
    private static ExecutorService executorService = Executors.newFixedThreadPool(1);

    public static void main(String[] args) throws Exception{
        System.out.println("----主线程启动");
        inheritableThreadLocal.set("主线程第一次赋值");
        System.out.println("----主线程设置后获取值:" + inheritableThreadLocal.get());
        executorService.submit(new TestInheritableThreadLocalAndExecutor());
        System.out.println("主线程休眠2秒");
        Thread.sleep(2000);
        inheritableThreadLocal.set("主线程第二次赋值");
        executorService.submit(new TestInheritableThreadLocalAndExecutor());
        executorService.shutdown();
    }

    @Override
    public void run() {
        System.out.println("----子线程获取值:" + inheritableThreadLocal.get());
    }
}

线程池中如何实现ThreadLocal的数据传递?

如果涉及到线程池使用ThreadLocal, 必然会出现问题。首先线程池的线程是复用的,其次,比如你从Tomcat的线程到自己的业务线程,也就是跨线程池了,线程也就不是之前的那个线程了,也就是说ThreadLocal就用不了,那么如何解决呢?

可以使用阿里的ttl来解决
https://gitcode.net/mirrors/alibaba/transmittable-thread-local?utm_source=csdn_github_accelerator

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值