ThreadLocal详解

  • ThreadLocal解决了一个什么问题?

ThreadLocal 解决的核心问题是:在多线程环境下,为每个线程提供独立的变量副本,避免了线程间共享变量时的同步问题。实际上是提供了一种隔离机制。

  • ThreadLocal的底层原理?

ThreadLocal只是一个类,提供了ThreadLocalMap,真正的数据存储在了Thread线程对象内部的Map里,每个Thread都有一个这样的Map:ThreadLocal.ThreadLocalMap threadLocals
ThreadLocalMap底层并不是HashMap!而是一个数组+开放地址法的线性探测。(注意:HashMap使用的是数组+链表/红黑树,而ThreadLocalMap只使用了数组,当存在哈希冲突时,使用开放地址法的线性探测寻找下一个可用的哈希槽)。其中数组中存储的是Entry。

static class Entry extends WeakReference<ThreadLocal<?>> {
	/** The value associated with this ThreadLocal. */
	Object value;
	
	Entry(ThreadLocal<?> k, Object v) {
	   super(k);
	   value = v;
	}
	}

不难发现,Entry数组的key=当前ThreadLocal对象本身,不是线程id,也不是int,也不是String。value=设置的值。
因此一个线程里可以创建多个ThreadLocal:

ThreadLocal<String> userLocal = new ThreadLocal<>();
ThreadLocal<Integer> orderLocal = new ThreadLocal<>();
ThreadLocal<List> rolesLocal = new ThreadLocal<>();

当进行set(value)操作时:首先获取当前线程Thread,再得到当前Thread的ThreadLocalMap。对ThreadLocal对象本身进行hash,放入第一个不冲突的哈希槽中。
在这里插入图片描述

Thread.currentThread().threadLocals.put(thisThreadLocal, value)

执行get()操作时:首先根据hash得到index,查看index.key是不是自己,不是的话就顺序向下寻找。一般情况下的时间复杂度是O1。

  • ThreadLocal是线程安全的吗?

由于每个线程都有自己的ThreadLocalMap,不同的线程即使使用同一个ThreadLocal对象,它们操作的实际也是不同的存储空间(不同的Map)。因此,不存在多线程竞争同一个资源的情况,从数据存储和访问的角度来看,它是绝对线程安全的。
但当value引用了可变且非线程安全的对象是就不是线程安全的。ThreadLocal 只是为每个线程提供了独立的存储空间。但如果这个存储空间里存放的是一个非线程安全的、可变的共享对象,那么在这个线程内部,对该对象的操作依然需要同步。

private static ThreadLocal<List<String>> threadLocal = ThreadLocal.withInitial(ArrayList::new);

我们的后端程序本身就是一个多线程的。比如Spring Boot项目,默认情况下,每个HTTP请求是由一个独立的线程处理的(例如Tomcat的线程池中的线程)。在这种情况下,即使我们没有显式地创建线程,我们的程序也是运行在多线程环境下的。每个请求线程处理一个用户请求,多个请求同时到来时,服务器会使用多个线程同时处理。在这些多线程场景下,如果我们需要在同一个线程内共享某些数据,并且希望这些数据与其他线程隔离,那么ThreadLocal就派上用场了。

我们通常使用的Web服务器(如Tomcat、Jetty等)或应用框架(如Spring Boot)会内置一个线程池来处理请求。当请求到来时,服务器不会为每个请求都新建一个线程(因为线程创建和销毁的开销很大),而是从线程池中分配一个空闲的线程来处理该请求。处理完成后,线程会被释放回线程池,以便重复使用。因此,每个请求通常由一个独立的线程来处理。

以Spring Boot应用为例,当我们启动一个Spring Boot应用时,默认内嵌了Tomcat服务器。Tomcat会启动一个线程池(默认大小是200),当有请求到来时,Tomcat会从线程池中取出一个线程来执行我们的Servlet(比如DispatcherServlet),从而进入我们的业务代码。

即使连接可能存在复用情况,仍然会为每个请求分配一个线程池中的线程来处理。即连接的复用与线程无关。

  • ThreadLocal在登录校验中的作用

我们会设置拦截器对请求进行拦截,在拦截器中进行JWT的解析和验证,将解析出来的JWT中包含的信息放入ThreadLocal中,后续可供Service等业务层使用。
这样在后续的业务代码中可以直接从ThreadLocal中获取用户信息,而无需每次从JWT令牌中解析或者读取数据库。
每次请求都会使用新线程处理,因此当前请求结束之前应使用remove删除ThreadLocal。

  • 存放位置

堆(Heap):存储所有对象实例(包括对象本身的实例变量)和数组。
栈(Stack):每个线程拥有自己的栈,用于存储局部变量、方法调用和部分中间结果。栈中存储的是基本数据类型的值和对象引用(即对象在堆中的地址)。

public class StackExample {
    public void method() {
        // 局部基本类型变量 - 存储在栈中
        int count = 0;           // 在栈中
        boolean flag = true;     // 在栈中
        double price = 99.99;    // 在栈中
        
        // 局部对象引用 - 引用本身在栈中,对象在堆中
        String localStr = "test"; // 引用在栈,String对象在堆
        List<String> localList = new ArrayList<>(); // 引用在栈,ArrayList在堆
        
        // 方法参数 - 在栈中
        processData(count, localStr);
        
        // 方法调用信息 - 在栈中
        // 包括返回地址、局部变量表、操作数栈等
    }
    
    private void processData(int param1, String param2) {
        // 参数在栈中
    }
}

ThreadLocal实例本身存储在堆中(作为类的静态变量或实例变量),但是其引用存储在栈中:
在这里插入图片描述
每个Thread对象内部都有一个ThreadLocalMap,这个Map的key是ThreadLocal对象(弱引用),value是设置的值(强引用)。因此如果不手动remove,会导致内存泄漏!

  • 子线程能得到父线程的ThreadLocal值吗?

默认当然不能!线程之间是隔离的,ThreadLocal又是存储在Thread对象内部,每个线程拥有自己的ThreadLocalMap,彼此之间不共享。
但是可以通过InheritableThreadLocal实现在创建子线程时将父线程的数据拷贝一份,注意是拷贝!不是共享!

static ThreadLocal<String> tl = new InheritableThreadLocal<>();
  • ThreadLocalMap中,为什么key是弱引用,value是强引用?

首先回顾一下:每个线程拥有一个Thread,Thread中存储了ThreadLocalMap,其中key=ThreadLocal对象,通常ThreadLocal对象的定义是static,即所有线程共享,那么多线程根据ThreadLocal拿到的value一样吗?当然不一样!因为你根据ThreadLocal对象是去每个线程的ThreadLocalMap中找value,这个map又是存储在Thread类的内部的一个entry数组,因此线程间隔离。
因此:key是被两个所引用:map的entry数组+ThreadLocal对象;同样value也将会被两个引用:map的entry数组和使用这个value的。例如:

ThreadLocal<User> userThreadLocal = new ThreadLocal<>();
userThreadLocal.set(new User("Alice")); // 引用1
User user = userThreadLocal.get(); // 引用2

再回顾一下entry数组:

static class ThreadLocalMap {
    static class Entry extends WeakReference<ThreadLocal<?>> {
        Object value;
        Entry(ThreadLocal<?> k, Object v) {
            super(k);
            value = v;
        }
    }
}

可以看到,WeakReference<ThreadLocal<?>>,ThreadLocal对象,即key是弱引用。why?
在不使用线程池的情况下是没问题的,ThreadLocalMap的生命周期和Thread一样长,new Thread之后,线程执行完就自动释放了,此时ThreadLocalMap也会随之清除。但是现在大多采用线程池复用线程,例如之前提到的Spring boot中的tomcat服务器,当前线程执行完这个任务后不会释放,而是复用这个线程执行下个任务,那么此时如果不清理,就会导致大量无意义的键值对占用内存,导致内存泄漏。例如:

Thread → ThreadLocalMap → Entry(key=ThreadLocal, value=Object)

key 是强引用,ThreadLocal 永远不会被 GC。因此会将key设置成弱引用:当没有ThreadLocal指向它时,此时只有entry数组指向了他,而又是一个弱引用,可以被GC回收。此时就会有一堆的<null,value>在内存中。不过不用担心,在下次操作这个ThreadLocalMap(get/set/remove)时会自动清理key=null的键值对。

那么为什么value不也设置成一个弱引用?回顾前面的例子:

ThreadLocal<User> userThreadLocal = new ThreadLocal<>();
userThreadLocal.set(new User("Alice")); // 语句1
User user = userThreadLocal.get(); // 语句2

如果value是弱引用,程序此时刚好执行到第一条语句1时,发生了GC回收,实际上这个User还没来得及被语句2强引用就被GC回收了,那在执行语句2时就会得到一个null值,这显然与ThreadLocal的设计目标:“为每个线程保存一个稳定、可用的线程局部变量”相悖。
但是value强引用会存在一个问题:当前线程长期存活(比如线程池中的线程),但ThreadLocalMap 没有触发清理(没有 set/get/remove 操作),那么就会长期占用内存。因此建议在使用完ThreadLocal后务必remove!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值