1.ThreadLocal
1.1为什么要在登录拦截器中使用ThreadLocal存储用户信息
ThreadLocal是线程独立的,使用ThreadLocal方便在各个线程设置一个存储空间用来存放数据,而不必在每个方法中都显式地传递用户信息参数。 大部分使用场景是将用户信息存储到ThreaLocal。
1.2前置知识(强引用、软引用、弱引用、虚引用)
1.3代码原理
结合工作原理理解代码。
ThreadLocal:
ThreadLocal
类提供了get()
和set(T value)
方法,用于获取和设置线程局部变量的值。- 当你调用
ThreadLocal
的get()
方法时,它实际上是从当前线程中获取相应的值。 - 每个线程都持有一个
ThreadLocalMap
实例,该实例存储了所有ThreadLocal
变量的副本。
ThreadLocalMap:
ThreadLocalMap
是ThreadLocal
的内部静态类,它是一个自定义的哈希表,专门用于存储每个线程的局部变量。ThreadLocalMap
使用ThreadLocal
作为键,值为具体的线程局部变量。- 每个线程对象 (Thread) 内部有一个
ThreadLocal.ThreadLocalMap
类型的字段,用于存储与该线程关联的所有ThreadLocal
变量。 - 通过这种方式,每个线程都有自己的一套
ThreadLocal
变量副本,而这些变量对于其他线程是不可见的。
Entry:
ThreadLocalMap
中的键值对由Entry
类表示,Entry
类继承自WeakReference<ThreadLocal<?>>
,键是ThreadLocal
实例,值是对应的线程局部变量。- 使用弱引用的原因是防止内存泄漏:当一个
ThreadLocal
实例不再被使用时,即使它的键仍然存在于ThreadLocalMap
中,它也可以被垃圾回收器回收。
ThreadLocal创建
public class UserHolder {
private static final ThreadLocal<UserDTO> tl = new ThreadLocal<>();
public static void saveUserDTO(UserDTO user){
tl.set(user);
}
public static UserDTO getUserDTO(){
return tl.get();
}
public static void removeUserDTO(){
tl.remove();
}
}
t1.set方法(ThreadLocal.set)
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
map.set(this, value);
} else {
createMap(t, value);
}
}
Thread t = Thread.currentThread();当前线程
getMap(t);将当前线程传入
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
threadLocals是一个ThreadLocalMap
ThreadLocal.ThreadLocalMap threadLocals = null;
ThreadLocalMap继承了WeakReferenc弱引用
static class ThreadLocalMap {
/**
* The entries in this hash map extend WeakReference, using
* its main ref field as the key (which is always a
* ThreadLocal object). Note that null keys (i.e. entry.get()
* == null) mean that the key is no longer referenced, so the
* entry can be expunged from table. Such entries are referred to
* as "stale entries" in the code that follows.
*/
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
t1.set方法判断完之后 if (map != null) {
map.set(this, value);
private void set(ThreadLocal<?> key, Object value) {
// We don't use a fast path as with get() because it is at
// least as common to use set() to create new entries as
// it is to replace existing ones, in which case, a fast
// path would fail more often than not.
Entry[] tab = table;
int len = tab.length;
int i = key.threadLocalHashCode & (len-1);
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
ThreadLocal<?> k = e.get();
if (k == key) {
e.value = value;
return;
}
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}
1.4使用ThreadLocal为什么会有内存泄漏?
-
当
ThreadLocal
实例不再被引用时,弱引用会被回收,此时Entry
的键会变成null
。但Entry
的值仍然存在,且没有及时被清理,这就可能导致内存泄漏。 -
当线程长时间存在时,例如在使用线程池的情况下,线程不会被立即销毁,
ThreadLocalMap
也会一直存在。这会导致即使ThreadLocal
被回收,ThreadLocalMap
中的值也不会被及时清理,导致内存泄漏。ThreadLocal.remove()
方法将ThreadLocal中的Entry删除。
!!!重点
ThreadLocalMap
使用弱引用存储 ThreadLocal
键,但它的值(即线程局部变量)是强引用的。这导致即使 ThreadLocal
实例被垃圾回收,ThreadLocalMap
中的条目(Entry)仍然保持对值的强引用,从而导致值不能被自动回收,直到显式调用 remove()
或线程结束。
这就是为什么Thread为什么会存在内存泄漏。
2.共享session实现登录
(验证码 用户信息)
在用户输入电话号码之后,获取短信验证码。后端将生成的验证码保存到session中,之后用户验证可以通过设置的key值对于的value进行对比验证。
将用户登录信息保存到session中,是为在登录拦截器中进行身份验证。如何session中保存了用户的登录信息,则说明已登录。然后将用户想你想保存到ThreadLocal中。
3.使用redis替代共享session方式
(验证码 用户信息存储到redis中数据类型的选择)
(刷新过期时间)
(token存储到浏览器客户端)
因为在现实环境下,会部署多台Tomcat服务器,而在不同的Tomcat服务器之间是不能够互相通信的(session之间不能互相共享)。当切换服务器时,即使已登录,却找不到登录信息,判断未登录。
保存到redis时,验证码使用set存储。
保存用户登录信息到redis,使用hash数据结构(类似java中的map)。