ThreadLocal
ThreadLocal
ThreadLocal
是 Java 中的一个类,用于创建线程局部变量。每个线程都有自己的变量副本,互不干扰。ThreadLocal
可以用于在多线程环境下保持线程间独立的数据,常见的使用场景包括:
- 线程安全的数据共享:
ThreadLocal
可以用于在多线程环境下安全地共享数据,每个线程拥有自己的数据副本,互不影响。 - 上下文传递: 可以使用
ThreadLocal
传递上下文信息,避免显式传递参数的麻烦。例如,在Web应用中,可以将用户身份信息、请求信息等存储在ThreadLocal
中,方便在各个层次的代码中访问。 - 避免传递参数的复杂性: 在某些情况下,将参数传递给每个方法都会显得繁琐,使用
ThreadLocal
可以避免这种复杂性,因为数据被存储在线程本地。 - 线程池场景: 在使用线程池时,可以使用
ThreadLocal
存储一些线程私有的状态,而不需要担心线程复用时数据混乱。
1 |
|
从 Thread
类源代码入手
1 |
|
从上面Thread
类 源代码可以看出Thread
类中有一个 threadLocals
和 一个 inheritableThreadLocals
变量,它们都是 ThreadLocalMap
类型的变量,我们可以把 ThreadLocalMap
理解为ThreadLocal
类实现的定制化的 HashMap
。默认情况下这两个变量都是 null,只有当前线程调用 ThreadLocal
类的 set
或get
方法时才创建它们,实际上调用这两个方法的时候,我们调用的是ThreadLocalMap
类对应的 get()
、set()
方法
ThreadLocal的数据结构

每个Thread
中都具备一个ThreadLocalMap
,而ThreadLocalMap
可以存储以ThreadLocal
为 key ,Object 对象为 value 的键值对。
1 |
|
比如我们在同一个线程中声明了两个 ThreadLocal
对象的话, Thread
内部都是使用仅有的那个ThreadLocalMap
存放数据的,ThreadLocalMap
的 key 就是 ThreadLocal
对象,value 就是 ThreadLocal
对象调用set
方法设置的值。
ThreadLocalMap
有自己的独立实现,可以简单地将它的key
视作ThreadLocal<?>
,value
为代码中放入的值(实际上key
并不是ThreadLocal
本身,而是它的一个弱引用)。
每个线程在往ThreadLocal
里放值的时候,都会往自己的ThreadLocalMap
里存,读也是以ThreadLocal
作为引用,在自己的map
里找对应的key
,从而实现了线程隔离。
ThreadLocalMap
有点类似HashMap
的结构,只是HashMap
是由数组+链表实现的,而ThreadLocalMap
中并没有链表结构。我们还要注意Entry
, 它的key
是ThreadLocal<?> k
,继承自WeakReference
, 也就是我们常说的弱引用类型。
总结:
- 每个Thread线程内部都有一个ThreadLocalMap。
- Map里面存储线程本地对象ThreadLocal(key)和线程的变量副本(value)。
- Thread内部的Map是由ThreadLocal维护,ThreadLocal负责向map获取和设置线程的变量值。
- 一个Thread可以有多个ThreadLocal。
ThreadLocal 内存泄露问题是怎么导致的?
ThreadLocalMap
中使用的 key 为 ThreadLocal
的弱引用,而 value 是强引用。所以,如果 ThreadLocal
没有被外部强引用的情况下,在垃圾回收的时候,key 会被清理掉,而 value 不会被清理掉。
这样一来,ThreadLocalMap
中就会出现 key 为 null 的 Entry。假如我们不做任何措施的话,value 永远无法被 GC 回收,这个时候就可能会产生内存泄露。ThreadLocalMap
实现中已经考虑了这种情况,在调用 set()
、get()
、remove()
方法的时候,会清理掉 key 为 null 的记录。使用完 ThreadLocal
方法后最好手动调用remove()
方法
1 |
|
弱引用介绍:
如果一个对象只具有弱引用,那就类似于可有可无的生活用品。弱引用与软引用的区别在于:只具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它 所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线程, 因此不一定会很快发现那些只具有弱引用的对象。
弱引用可以和一个引用队列(ReferenceQueue)联合使用,如果弱引用所引用的对象被垃圾回收,Java 虚拟机就会把这个弱引用加入到与之关联的引用队列中。
为了避免 ThreadLocal
内存泄露问题,可以采取以下一些建议:
- 及时清理: 在使用完
ThreadLocal
后,及时调用remove
方法清理。可以使用try-with-resources
或者finally
块确保在线程结束时调用remove
为什么ThreadLocal的key是弱引用?
线程在我们应用中,常常是以线程池的方式来使用的,比如 Tomcat 的线程池处理了一堆请求,而线程池中的线程一般是不会被清理掉的,所以这个引用链就会一直在,那么 ThreadLocal 对象即使没有用了,也会随着线程的存在,而一直存在着。所以这条引用链需要弱化一下,而能操作的只有 Entry 和 key 之间的引用,所以它们之间用弱引用来实现。

另一条引用链就是栈上的 ThreadLocal 引用指向堆中的 ThreadLocal 对象,这个引用是强引用。如果有这条强引用存在,那说明此时的 ThreadLocal 是有用的,此时如果发生 GC 则 ThreadLocal 对象不会被清除,因为有个强引用存在。
当随着方法的执行完毕,相应的栈帧也出栈了,此时这条强引用链就没了,如果没有别的栈有对 ThreadLocal 对象的引用,那么说明 ThreadLocal 对象无法再被访问到(定义成静态变量的另说)
那此时 ThreadLocal 只存在与 Entry 之间的弱引用,那此时发生 GC 它就可以被清除了,因为它无法被外部使用了,那就等于没用了,是个垃圾,应该被处理来节省空间。
至此,想必你已经明白为什么 Entny 和 key之间要设计为弱引用,就是因为平日线程的使用方式基本上都是线程池,所以线程的生命周期就很长,可能从你部署上线后一直存在,而 ThreadLocal 对象的生命周期可能没这么长。
所以为了能让已经没用 ThreadLocal对象得以回收,所以 Entry 和 key 要设计成弱引用,不然 Entry 和 key是强引用的话ThreadLocal 对象就会一直在内存中存在。但是这样设计就可能产生内存泄漏。
ThreadLocal的应用场景
- 用户会话信息存储:在 Web 应用中,每个用户的会话信息(如用户ID、权限、购物车等)可以通过
ThreadLocal
存储,确保每个线程在后续的请求都能访问正确并快速的访问用户会话信息 - 数据库连接:在多线程环境中,每个线程可以独立管理自己的数据库连接,通过
ThreadLocal
存储,这样可以避免数据库连接的频繁创建和销毁,提高性能。比如MyBatis中的SqlSession对象就使用了ThreadLocal来存储当前线程的数据库会话信息 - 事务管理:在分布式系统中,事务的上下文(如事务ID、当前状态等)可以通过
ThreadLocal
存储,每个线程独立的控制自己的事务,保证事务的隔离性。Spring中的TransactionSynchronizationManager类就是用ThreadLocal来存储事务相关的上下文信息。 - 日志记录:在多线程应用中,可以通过
ThreadLocal
存储日志记录器(Logger)的实例,确保每个线程的日志记录不会相互干扰。