基本使用
在项目中我们可以通过 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.ThreadLocalMap
ThreadLocal.ThreadLocalMap
内存储Entry[]
,因为一个线程可能使用了多个ThreadLocal
ThreadLocal
对象被作为 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博客