ThreadLocal & Memory Leak
基本使用
在项目中我们可以通过 ThreadLocal 来存储用户信息
其中一般会在过滤器/拦截器的入口处初始化用户信息,并在执行结束后对其进行清理
这样从请求进来一直到返回,我们只需要通过线程变量
ThreadLocal
获取用户信息即可,而不用每次都从数据库查出来
因为 ThreadLocal
是线程安全的,所以通常声明为一个静态单例变量
1 | public class UserHolder { |
就可以在拦截器中通过 set()
方法存储鉴权成功的用户数据,在业务逻辑中通过 get()
获取用户数据了
实现原理
This class provides thread-local variables. These variables differ from their normal counterparts in that each thread that accesses one (via its {@code get} or {@code set} method) has its own,independently initialized copy of the variable
构造方法
构造方法
ThreadLocal 仅存在一个无参构造方法
1 | public ThreadLocal() { |
设置值
set()
使用 set() 方法向其赋值
1 | public void set(T value) { |
首先使用 Thread.currentThread()
获取当前线程;Thread.currentThread() 是一个 native 方法
ThreadLocalMap
接下来调用了 getMap(Thread t) 方法
1 | /** |
返回的是线程对象的一个线程变量
ThreadLocal.ThreadLocalMap threadLocals = null,初始的默认值是
null,返回后回到 set()
当拿到的默认值是 null 时,则会创建一个 map 将值放入 map 中;不为 null 时将值直接放入 map
ThreadLocalMap 是线程安全的,除此之外在这里可以先视作
HashMap,也是基于散列存储的 Map
createMap()
当第一次使用 ThreadLocalMap 时,需要调用
createMap() 进行创建
createMap() 内调用了 ThreadLocalMap
的构造方法,把自己线程对象作为参数传了进去
1 | void createMap(Thread t, T firstValue) { |
1 | private static final int INITIAL_CAPACITY = 16; |
除去创建 Entry 数组对象和重置负载因子的操作,本质上将 KV
根据线程对象的 threadLocalHashCode
放到了相应的数组位置上,K 就是线程对象,V 是 ThreadLocal
要存储的值
所以需要注意:
ThreadLocalMap中的 K 是线程对象,V 是要存储的值- 所有的线程都在使用同一个
ThreadLocalMap对象,K 是各自的线程对象
弱引用
1 | static class Entry extends WeakReference<ThreadLocal<?>> { |
ThreadLocal.ThreadLocalMap 内存储的是
Entry[]
Entry 的 key 是一个弱引用,而 value 为强引用
取值
get()
1 | public T get() { |
获取的方法,首先获取当前线程拥有的
ThreadLocalMap,然后将自己对象作为 K 进行取值
如果 ThreadLocalMap 不存在,则进行初始化
setInitialValue(),初始化结束后返回 null
所以也可以通过继承 ThreadLocal 后重写
initialValue() 来设置默认的返回值
删除值
remove()
1 | public void remove() { |
先获取线程对象中保存的 ThreadLocalMap,如果不为 null
则调用 remove()
ThreadLocalMap 的 remove()
1 | private void remove(ThreadLocal<?> key) { |
简而言之就是以线程对象作为 K 对 V 进行删除
总结
比较反直觉的是,操作 ThreadLocal 对象,但数据并不存储在
ThrealLocal,而是存储在线程对象的
ThreadLocal.ThreadLocalMap 中
ThreadLocal更像是一个工具类,用来操作ThreadLocal.ThreadLocalMapThreadLocal.ThreadLocalMap内存储Entry[],因为一个线程可能使用了多个ThreadLocalThreadLocal对象被作为 key 进行使用ThreadLocal.ThreadLocalMap的 key 是弱引用,弱引用的目的是为了便于对ThreadLocal对象本身进行回收
内存泄漏
ThreadLocal 整个使用过程中会创建出哪些对象:
ThreadLocal对象ThreadLocal.ThreadLocalMap对象,在Thread对象中被持有ThreadLocal.ThreadLocalMap中的Entry对象ThreadLocal.ThreadLocalMap中的Entry对象中的 K 和 V
ThreadLocal
ThreadLocal 对象一般不存在泄漏问题:
ThreadLocal是操作ThreadLocal.ThreadLocalMap的 key,实例化数量不多Entry弱引用的设计就是为了及时回收ThreadLocal对象
ThreadLocal.ThreadLocalMap
基本也不会存在泄漏问题,该对象被 Thread
对象持有,并且只是一个 Map 结构的引用
Entry
Entry 本身是一个引用对象,其通过
ThreadLocal.ThreadLocalMap 的操作方法进行释放,例如
set、remove 中的
replaceStaleEntry、expungeStaleEntry 方法
Entry 对象的 key 即为 ThreadLocal
对象,上面已经提到了因为弱引用的设计一般会被及时回收
Entry 对象的 value 为强引用,它只会因为
Entry
对象的回收而被回收,这是最有可能发生内存泄漏的地方,可能存在作为 key 的
ThreadLocal 对象已经被回收,但是 value 无法回收的情况
当然在设计中,ThreadLocal.ThreadLocalMap
的一些操作会检查整个
Map(replaceStaleEntry、expungeStaleEntry),从而对
key 已经回收的 Entry 进行释放,避免内存泄漏
总结
上述可见,Entry 时最有可能会造成内存泄漏的地方
- 没有手动调用
remove方法,同时ThreadLocal无论是否回收,value 都可能在一定时间内、或者一直无法被回收 - 错误地创建
Thread对象,而没有对旧的线程对象进行回收 - 这里有一个 Tomcat 机制相关的问题从而导致泄漏,我并没看懂... java - ThreadLocal & Memory Leak - Stack Overflow
参考
为什么ThreadLocal是线程安全的? - 掘金 (juejin.cn)
Tomcat线程复用与Threadlocal引发的惨案_线程复用 threadlocal_小沈同学呀的博客-CSDN博客