原文地址:http://www.linzichen.cn/article/1586752807401160704
前言
在后端接口中,从接收到请求直到响应结束,我们希望能随时获取到当前用户的信息,所以一般会在网关的拦截器中解析用户 token, 并将解析结果临时存储起来,从而实现在整个业务中能随时读取。
问题:获取用户信息后,存储在哪里?
- 方式一:可以将解析出来的用户信息存储在
request对象中,然后在业务里通过request.getAttribute()来获取。这种方式需要在每个方法形参上都加上HttpServletRequest对象,需要更改形参列表,耦合性太高,虽然可以实现,但是不推荐。 - 方式二:创建一个全局唯一
map,解析用户token后将用户信息存储到全局map中,在业务里通过map.get()获取。此方式在单线程下没有问题,但是多线程下修改的也是同一个map,会造成线程不安全问题。
ThreadLocal引出
针对上述方案二的问题,如果能够在每个线程内部自己维护一个map,这样每个线程之间数据彼此独立,可以做到线程隔离,就避免了线程安全问题。而 ThreadLocal 就可以很好地帮我们实现这个功能。
Map问题代码
先看下用一个全局 map 存储用户信息产生的问题:
public class ThreadLocalDemo {
static Map<String,User> userMap = new HashMap<>() ;
public static void main(String[] args) {
new Thread(() -> {
userMap.put("userInfo",new User(1, "张三")) ;
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
}
System.out.println(Thread.currentThread().getName() + ":" + userMap.get("userInfo"));
}, "张三线程").start();
new Thread(() -> {
userMap.put("userInfo",new User(2, "李四")) ;
System.out.println(Thread.currentThread().getName() + ":" + userMap.get("userInfo"));
},"李四线程").start();
}
}
class User {
Integer id ;
String name ;
User (Integer id, String name) {
this.id = id ;
this.name = name ;
}
@Override
public String toString() {
return "User{" +
"id=" + id +
", name='" + name + '\'' +
'}';
}
}
打印结果如下:
李四线程:User{id=2, name='李四'}
张三线程:User{id=2, name='李四'}
在上面代码中,用两个线程模拟了用户同时访问系统的过程,可以发现 张三线程在 2S 之后再获取信息时,已经被李四线程修改了,造成了线程不安全现象。
ThreadLocal实现
ThreadLocal 类中提供了实例对象的 set() 和 get() 方法来存储当前线程的变量。所以我们可以通过其 set()方法存储用户信息,用其get()方法来获取用户信息。
public class ThreadLocalDemo {
static ThreadLocal<User> threadLocal = new ThreadLocal<>() ;
public static void main(String[] args) {
new Thread(() -> {
threadLocal.set(new User(1, "张三"));
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
}
System.out.println(Thread.currentThread().getName() + ":" +threadLocal.get());
}, "张三线程").start();
new Thread(() -> {
threadLocal.set(new User(2, "李四"));
System.out.println(Thread.currentThread().getName() + ":" + threadLocal.get());
},"李四线程").start();
}
}
class User {
Integer id ;
String name ;
User (Integer id, String name) {
this.id = id ;
this.name = name ;
}
@Override
public String toString() {
return "User{" +
"id=" + id +
", name='" + name + '\'' +
'}';
}
}
运行结果如下:
李四线程:User{id=2, name='李四'}
张三线程:User{id=1, name='张三'}
通过代码逻辑不难看出,张三线程 先往 ThreadLocal 中存储了张三用户信息,没有立即取,而是等待了 2 秒钟,在此期间 李四线程 也向 ThreadLocal 中存储了李四用户信息。2秒钟之后张三线程 获取到的仍是自己当时存储的用户,没有像 Map案例 似的被替换成了李四。
也就是说,ThreadLocal 之间的线程变量是互相隔离的。接下来就看下 ThreadLocal 是如何现在的。
ThreadLocal分析
ThreadLocal 是如何做到线程之间变量隔离的。通过查看源码,我们发现在每个 Thread中存在一个属性 threadLocals,其类型是 ThreadLocalMap。也就是说,每 new 一个线程实例时,都会有对应的一个 ThreadLocalMap 对象。

而我们再调用 ThreadLocal 实例的的 get() 和 set(),通过获取到当前线程的 ThreadLocalMap 对象,分别通过此对象的 getEntry() 和 set() 来获取和存储数据。

所以说我们通过 ThreadLocal 实例进行存储和获取时,实际上是通过操作的每一个线程对象自身的一个 ThreadLocalMap 来实现的,从而实现了每个线程之间互不影响,互相隔离。
ThreadLocalMap
由上面可知,每一个线程通过自身的 ThreadLocalMap对象来完成数据存储,所以下面主要看下 ThreadLocalMap 到底是什么。
打开源码发现,ThreadLocalMap 里面包含一个静态的内部类 Entry ,该类继承于WeakReference类,说明Entry是一个弱引用。
ThreadLocalMap内部还包含了一个Entry数组,其中:Entry = ThreadLocal + value。
借用网上一张图,从宏观上认识一下 Thread 和 ThreadLocalMap 及 Entry 的关系:

从上图中看出,在每个Thread类中,都有一个ThreadLocalMap的成员变量,该变量包含了一个Entry数组,该数组真正保存了ThreadLocal类set的数据。Entry是由threadLocal和value组成,其中threadLocal对象是弱引用,在GC的时候,会被自动回收。而value就是ThreadLocal类set的数据。
下面用一张图总结下引用关系:

上图中除了Entry的key对ThreadLocal对象是弱引用,其他的引用都是强引用。
Entry的key为什么设计成弱引用
我们都知道ThreadLocal变量对ThreadLocal对象是有强引用存在的。即使ThreadLocal变量生命周期完了,设置成null了,但由于key对ThreadLocal还是强引用。
此时,如果执行该代码的线程使用了线程池,一直长期存在,不会被销毁。就会存在这样的强引用链:Thread变量 -> Thread对象 -> ThreadLocalMap -> Entry -> key -> ThreadLocal对象。
那么,ThreadLocal对象和ThreadLocalMap都将不会被GC回收,于是产生了内存泄露问题。
为了解决这个问题,JDK的开发者们把Entry的key设计成了弱引用。
弱引用的对象,在GC做垃圾清理的时候,就会被自动回收了。
如果key是弱引用,当ThreadLocal变量指向null之后,在GC做垃圾清理的时候,key会被自动回收,其值也被设置成null,在一定程度上可以减少内存泄漏问题。
如下图所示:

key 为 null 的问题
如果当前Thread 运行结束,那么 ThreadLocal、ThreadLocalMap和Entry 都没有强引用与之关联了,在 GC的时候都会被回收。
但在实际开发中,我们一般都会采用线程池的方式来维护线程,为了复用线程是不会结束的。这样一来,每个线程的 ThreadLocalMap 中就会出现 key 为 null 的 Entry,我们就没有办法访问这些value,且这些 value 会一直存在一条强引用链,造成 内存泄漏。

虽然弱引用,保证了key指向的ThreadLocal对象能被及时回收,但是value指向的T对象是无法被回收的,因此弱引用不能100%保证内存不泄露。
我们要在不使用某个ThreadLocal对象后,手动调用remoev()方法来删除它,尤其是在线程池中,不仅仅是内存泄露的问题,因为线程池中的线程是重复使用的,意味着这个线程的ThreadLocalMap对象也是重复使用的,如果我们不手动调用remove()方法,那么后面的线程就有可能获取到上个线程遗留下来的value值,造成bug。
清除脏 Entry
实际上,线程在调用 ThreadLocal 的 set()、get() 和 remove() 方法时,在 ThreadLocal 声明周期里,针对内存泄漏问题,都会通过 expungeStaleEntry() 、cleanSomeSlots和replaceStaleEntry() 方法来清理掉 key 为 null 的脏 entry。
总结
-
ThreadLocal并不解决线程间共享数据的问题。
-
ThreadLocal适用于变量在线程间隔离且在方法间共享的场景。
-
ThreadLocal通过隐式的在不同线程内创建独立实例副本避免了实例线程安全的问题。
-
每个线程持有一个只属于自己的专属Map并维护了ThreadLocal对象与具体实例的映射,该Map由于只被持有它的线程访问,故不存在线程安全以及锁的问题。
-
ThreadLocalMap的Entry对ThreadLocal的引用为弱引用,避免了ThreadLocal对象无法被回收的问题都会通过expungestaleEntry,cleanSomeSlots,replacestaleEntry这三个方法回收键为null的Entry对象的值(即为具体实例)以及Entry对象本身从而防止内存泄漏,属于安全加固的方法。
群雄逐鹿起纷争,人各一份天下安!
本文介绍了ThreadLocal的基本概念,分析了其内部实现机制,并探讨了如何利用ThreadLocal解决多线程环境下用户信息存储的安全问题。
189

被折叠的 条评论
为什么被折叠?



