为什么要使用 ThreadLocal 进行登录时处理用户信息?而非普通变量?
假如有两个用户 A 和 B,他们分别进行登录,并且他们的每次请求都会带有自己的 token,在请求到达 controller 之前(preHandle() 中),每次都会被会被拦截器进行拦截,提取出当前 token 中的用户信息(比如 userId),认证通过以后在 service 中就可以通过 Contenxt 类获取提取出来的用户信息。
前提
- Context 类中存储用户的 ID,有一个静态的变量或者对象叫做 USER_ID。
(1)使用普通变量,例如 String
Context 类使用 String 变量 (static String USER_ID) 进行存储,这时候:
- 用户 A 登录,带着自己的 token,到达后端拦截器,token 验证通过后,用户信息被提取到 Context 中的 String 变量中。
- 用户 B 登录,带着自己的 token,到达后端拦截器,token 验证通过后,用户信息被提取到 Context 中的 String 变量中。(这里 String 变量之前存储的是 A 的信息,但是由于 B 登录以后,又将 String 的值设置为了 B 的 token 中提取出来的用户信息。)
- 用户 A 调用新增的 api,这时候调用新增 api 的这个请求,也附带了 A 的 token 信息,所以重复第一步。
- 用户 B 调用新增的 api,这时候调用新增 api 的这个请求,也附带了 B 的 token 信息,所以重复第二步。
虽然存储用户信息都是在一个 String 中,但是好像并没有发现什么问题。(往下看)
(2)使用 ThreadLocal 类进行存储
Context 类使用 ThreadLocal 类型的对象 (ThreadLocal USER_ID = new …) 进行存储,这时候:
- 用户 A 登录,带着自己的 token,到达后端拦截器,token 验证通过后,接下来应该提取用户信息到 Context 中了,这时候,用户 A 当前登录是在一个线程 ThreadA 中,那么看到 Context 中定义的 USER_ID 是 ThreadLocal 类型的,(简单讲)这时候他会以当前线程为 key = ThreadA,以 A.token 为 value 创建一个新的只属于当前 ThreadA 的对象 USER_ID。
- 用户 B 登录,带着自己的 token,到达后端拦截器,token 验证通过后,接下来应该提取用户信息到 Context 中了,这时候,用户 B 当前登录是在一个线程 ThreadB 中,那么看到 Context 中定义的 USER_ID 是 ThreadLocal 类型的,(简单讲)这时候他会以当前线程为 key= ThreadB,以 B.token 为 value 创建一个新的只属于当前 ThreadB 的对象 USER_ID。
总结
- 第一种方式看似运行时和第二种没有区别,但是在高并发的时候,由于 USER_ID 的值的设置和 USER_ID 的值的获取是两次操作,那么很显然设置和获取不是一个原子性的操作,这时候肯定会发生并发问题,即:A 刚设置了值,还没有等到 A 取值,B 就将这个 String 类型的 USER_ID 设置成了自己的信息。这时候 A 再进行取值,取到的就是 B 的值。
- 第二种方式的话,很显然就解决了这个问题,因为他们都是操作的自己线程内的 USRE_ID,各个线程之间互不影响,所以这个时候,完全不会混乱。
注意
- 首先说一个名词 OOM,即 Out Of Memory,内存泄露、内存溢出。
- ThreadLocal 中的 key 是弱引用,value 是强引用。
- 弱引用,自动垃圾回收。
- 强引用,线程销毁时,才会被回收。
- 一个线程可能有时候很久都不会被销毁,但是这时候只有弱引用的 key 会被回收,value 由于是强引用,由于线程还存在,他就会存在,但是 value 已经没有用了,这个时候就造成了浪费。
- 为了避免浪费内存,继而发生内存溢出,我们需要使用 remove() 方法,进行手动清除 ThreadLocal 对象。