- 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!
2383

被折叠的 条评论
为什么被折叠?



