🔅前言
ThreadLcoal也是我们在面试中出现频率很高的技术点,大多时候的回答也只是表面的概念并没有深入其中为了应付面试,这里我们打破砂锅学到底,真正把他弄清楚。
📶概述
这里简单介绍下ThreadLcoal
ThreadLocal 旨在解决多线程环境中的共享资源并发问题。它为每个线程提供一个独立的内存空间,以存储该线程的临界变量,这些变量也被称为线程的副本。由于每个线程都拥有自己的副本,因此线程之间互不干扰,从而消除了资源竞争和并发问题。
使用场景优势:简单来说,比如在业务逻辑中频繁使用到登录用户的基本信息时,我们可以在拦截器中把用户信息存储到一个定义好的上下文类的 ThreadLocal 中,这样在后续的业务逻辑中使用时,不需要重复访问数据库查询或通过方法参数传递数据,而是可以直接从上下文类获取已存入内存的数据,大大提高了效率。
🌐ThreadLocal api
private static ThreadLocal<LoginUser> threadLocal = new ThreadLocal();
- 存储数据
threadLocal.set("内容对象")
- 获取数据
threadLocal.get()
- 移除数据
threadLocal.rmove()
🙋🏻♀️我有疑惑
❓线程的副本是什么?ThreadLcoal怎么存储数据的
前面概念中提到的线程副本,在深入学习之前博主一直认为是为每一个线程创建一个单独的ThreadLcoal来存储数据,但实际上并不是这样的!
让我们进入threadLocal.set("内容对象")源码一探究竟
/*
* 位置:java.lang.ThreadLocal
* 行数:218
*/
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
map.set(this, value);
} else {
createMap(t, value);
}
}
通过源码可以知道
- 通过源码可知,创建的副本并不是
ThreadLcoal而是ThreadLocalMap,为每一个线程创建一个ThreadLocalMap对象来存储数据,而ThreadLocal只是提供给我们操作ThreadLocalMap的工具而已。在项目中使用过ThreadLocal的同学,大家不难发现定义ThreadLocal的时候是当做全局共享对象来使用的,使用static修饰提前定义在上下文类中。所有线程共享一个ThreadLocal,但ThreadLocal会为每一个线程创建一个独立的ThreadLocalMap对象来存储数据。 - 通过源码可知,
ThreadLocalMap是在线程调用set()或get()方法的时候才会去创建(概念中提到的“开辟内存空间”), 这种懒加载的机制就是为了节省内存——只有在需要使用线程本地变量时,才会给线程分配专门的内存空间来存储这些变量。
❓ThreadLocalMap怎么存储数据
ThreadLocalMap简单点说就是一个map对象,哪有就需要键值对key/value。
- key: 为
ThreadLocal对象本身。map.set(this, value)可知,this就是当前对象。 - value: 为我们需要存储的变量数据
❓不仅疑惑为什么用ThreadLocal作为一个线程的key呢
- 虽然
ThreadLocal是全局的,但是ThreadLocalMap确实每个线程单独的相互隔离,所以对于ThreadLocal作为key,相对单个线程而言在map中就是唯一的。 - 避免额外的键管理, 如果使用其他对象作为键,则需要在每个线程中管理这些对象的生命周期和引用。将
ThreadLocal作为键简化了这个过程,因为它的生命周期与线程紧密相关。 - 看得最多的一点,因为“
ThreadLocal在map中作为一个弱引用的key来使用的,可以避免潜在的内存泄漏问题。一旦线程不再使用它,它可能不再被强引用。在这种情况下,通过将ThreadLocal设置为弱引用,JVM 的垃圾回收器可以及时回收ThreadLocal对象,避免因为它还存在于ThreadLocalMap中而无法释放内存”。但ThreadLocal实例本身始终是强引用,只是作为key赋予了它当它的变量被置为null或类实例被销毁时,变成弱引用,然后被gc回收。所以作为keyThreadLocal不是必须的,博主更倾向于第2点解释。
❓ThreadLocalMap中key的弱引用是怎么实现的
继续深入源码
/*
* 位置:java.lang.ThreadLocal
* 行数:329
*/
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
我们知道map的数据结构中其中一个就是数组,通过hashCode来获得数组的下标。Entry对象就是ThreadLocalMap 数组中存储的对象。
Entry继承WeakReference<ThreadLocal>对象,WeakReference我们字面翻译都知道是弱引用啦
Entry对象中定义了一个变量value,用来存储用户的变量数据,是强引用。
而super(k);就是将ThreadLocal作为的key传递给父类,就此当达到一定条件后就拥有了弱引用的特性。
继续深入父类,我们发现最终变量被定义在抽象类Reference中
/*
* 位置:java.lang.ref
* 行数:153
*/
public abstract class Reference<T> {
private T referent; /* Treated specially by GC */
//...
}
扩展:
强引用: 在 Java 中,强引用是指普通的引用关系。只要有强引用指向一个对象,垃圾回收器就不会回收这个对象。即使系统内存紧张,这个对象也会一直存在。就如上面的给value赋完值一样,他就是强引用了。当然没有赋值的变量什么引用都不是。
弱引用: 创建一个类并继承 WeakReference,并在构造函数中调用 super() 将对象传递给 WeakReference 的构造函数时,您就创建了一个弱引用 。
除了上面两种引用,还有虚引用、软引用,不在本篇讨论中。
❓为什么使用 ThreadLocalMap 存储
前面说到了ThreadLocalMap 是一个Map,使用它的原因基本2点
- 灵活性:
ThreadLocalMap的存在是为了支持在一个线程中存储多个ThreadLocal对象的值。 一个线程可以同时有一个ThreadLocal存储用户信息,另一个ThreadLocal存储数据库连接。每个ThreadLocal都可以在同一个线程中存储自己的值,而不会互相影响。 - 避免全局变量冲突 :
ThreadLocalMap可以支持多个ThreadLocal对象,即使不同的库或模块使用各自的ThreadLocal对象,也不会产生冲突。每个ThreadLocal在ThreadLocalMap中是独立的,保证了数据的隔离性。
通过上面两点我们加深了对于ThreadLocalMap 的理解,那就是一个线程只会有一个ThreadLocalMap ,不管ThreadLocal 有多少个,ThreadLocal 只是用于操作ThreadLocalMap的工具类。
💭释放ThreadLocalMap 内存
通过上面的提问-回答的方式我们知道了ThreadLocalMap 是和线程的生命周期深度绑定的,线程存在那么ThreadLocalMap 这部分内存则已知存在,线程销毁则内存也随之释放。
但对于我们java开发中常用的spring boot框架开发的项目,尽管我们不额外显示的使用线程池,但是spring boot中内嵌了Tomcat服务器, 它也会为处理接口请求提供一个线程池。这种情况下,尽管你没有直接管理线程池,但请求处理仍然是通过线程池进行的。每个请求会在池中的一个线程中处理,从而实现线程的复用。
所以ThreadLocalMap 并不会随着接口方法的结束而释放内存。线程处理完接口逻辑后继续回到线程池等待下次请求。内存如果得不到有效的释放,大量堆积会导致程序出现内存泄露的风险,非常影响性能。
❓怎么才能快速有效的释放ThreadLocalMap 内存呢
那就是前面说到的ThreadLocalMap 的key,当作为key的ThreadLocal 对象为null或被摧毁后,那么key就会变成一个弱引用, 下次GC的时候它会检查 ThreadLocalMap 中的 弱引用键 , GC 发现 ThreadLocalMap 中的键(ThreadLocal)是一个弱引用,会将不再被引用的 ThreadLocal 对象回收,并将对应的键设为 null。 但是value是强引用,会一直存在。
(注意:在特殊情况下,代码可以遍历并清除 ThreadLocalMap 中的 null 键条目,但这种操作属于高级优化范畴,不建议常用,这里不展开讨论。而且对应弱引用的处理,因为GC是属于JVM控制的,我们并不可控,下次GC谁都不知道 。 )
‼️综上所诉,我们使用完ThreadLocal后,建议都要手动释放内存,调用 threadLocal.rmove() 方法。
扩展:如果我们在定义ThreadLocal时用了static修饰关键字,那么map中的key会一直是强引用,不会变成弱引用,因为使用static后,ThreadLocal属于当前类的,会被类持续强引用,而不再是属于一个实例或对象了,它跟随类的生命周期,类的生命周期是什么,那就是程序启动时类加载开始直到项目程序关闭。所以一定要手动释放 ****threadLocal.rmove() 。
✅应用
本案例是使用ThreadLocal作为存储登录用户基本信息使用,并在拦截器中存储接登录中的用户数据。
- 定义上下文工具类
public class SessionContext {
private ThreadLocal<LoginUser> threadLocal;
private SessionContext() {
this.threadLocal = new ThreadLocal<>();
}
/**
* 使用静态内部类创建单例
*/
private static class Context {
private static final SessionContext INSTANCE = new SessionContext();
}
public static SessionContext getInstance() {
return Context.INSTANCE;
}
public void set(LoginUser user) {
this.threadLocal.set(user);
}
public LoginUser get() {
return this.threadLocal.get();
}
public void clear() {
this.threadLocal.remove();
}
}
- 创建登录拦截器类,实现HandlerInterceptor类
@Component
public class TokenInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//...
LoginUser loginUserInfo = new LoginUser();
loginUserInfo.setUserId(1)
//用户信息设置到上下文
SessionContext.getInstance().set(loginUserInfo);
return HandlerInterceptor.super.preHandle(request, response, handler);
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
HandlerInterceptor.super.afterCompletion(request, response, handler, ex);
//释放缓存
SessionContext.getInstance().clear();
}
}
注:代码为简化版为了突出对ThreadLocal的使用,完整源码可参考:github地址 找到对应类进行查看。