背景说明
鉴于ThreadLocal的特性,ThreadLocal会给每个Thread线程单独分配一块变量副本,所以项目中很多地方会使用ThreadLocal进行处理,比如登录接口,每次请求进来后将操作员信息放到ThreadLocal中与当前线程进行绑定,当前线程任何时刻都能获取用户登录信息。
一:ThreadLocal的原理
1.1 什么是ThreadLocal变量
ThreadLoal 变量,线程局部变量,同一个 ThreadLocal 所包含的对象,在不同的 Thread 中有不同的副本。这里有几点需要注意:
- 因为每个 Thread 内有自己的实例副本,且该副本只能由当前 Thread 使用。这是也是 ThreadLocal 命名的由来。
- 既然每个 Thread 有自己的实例副本,且其它 Thread 不可访问,那就不存在多线程间共享的问题。
- ThreadLocal类型的变量一般用private static加以修饰,当一个线程结束时,它所使用的所有 ThreadLocal 相对的实例副本都可被回收。
ThreadLocal提供线程局部变量。这些变量与普通的变量不同之处在于,每个访问这种变量的线程(通过它的get或set方法)都有自己的、独立初始化的变量副本。
ThreadLocal实例通常是希望将状态关联到一个线程的类的私有静态字段(比如,user ID 或者 用户登录信息 等等)。
1.2 ThreadLocal实现原理
首先 ThreadLocal 是一个泛型类,保证可以接受任何类型的对象。
Thread类有一个类型为ThreadLocal.ThreadLocalMap的实例变量threadLocals,也就是说每个线程有一个自己的ThreadLocalMap,一个线程内可以存在多个 ThreadLocal 对象,所以其实是 ThreadLocal 内部维护了一个 Map ,这个 Map 不是直接使用的 HashMap ,而是 ThreadLocal 实现的一个叫做 ThreadLocalMap 的静态内部类。而我们使用的 get()、set() 方法其实都是调用了这个ThreadLocalMap类对应的 get()、set() 方法。例如下面的 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);
}
}
get方法
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
threadLocalMap静态类
static class ThreadLocalMap {
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
............
............
}
1.3 ThreadLocalMap实现原理
Thread类有一个类型为ThreadLocal.ThreadLocalMap的实例变量threadLocals,也就是说每个线程有一个自己的ThreadLocalMap。ThreadLocalMap有自己的独立实现,可以简单地将它的key视作ThreadLocal,value为代码中放入的值(实际上key并不是ThreadLocal本身,而是它的一个弱引用
)。每个线程在往某个ThreadLocal里塞值的时候,都会往自己的ThreadLocalMap里存,读也是以某个ThreadLocal作为引用,在自己的map里找对应的key,从而实现了线程隔离。
1.4 ThreadLocalMap结构图
ThreadLocalMap维护了Entry环形数组,数组中元素Entry的逻辑上的key为某个ThreadLocal对象(实际上是指向该ThreadLocal对象的弱引用
),value为代码中该线程往该ThreadLoacl变量实际塞入的值。默认初始大小为16
,当Entry数组存储阀值达到2/3
的时候会以2的倍数进行扩容
二:ThreadLocal的使用
引入一个项目中实际使用的示例,请求进入时携带登录用户的token,使用拦截器拦截所有请求,然后通过token获取到登录用户的详细信息,将用户信息存放到ThreadLocal中,与当前线程绑定,后续处理逻辑时可以通过ThreadLocal获取到用户登录信息。
2.1 创建ThreadLocal工具类
public class LoginUtil {
private LoginUtil() {}
private static final ThreadLocal<UserDto> userLocal = new ThreadLocal<>();
public static UserDto getUser() {
return userLocal.get();
}
public static void setUser (UserDto user) {
userLocal.set(user);
}
public static void remove () {
userLocal.remove();
}
2.2 在拦截器/过滤器中通过用户token获取登录信息,并与当前线程绑定
LoginUtil.setUser(loginMapper.findUserDtoByid(userId));
2.3 业务处理时获取用户信息
UserDto userDto = LoginUtil.getUser();
2.4 当前请求结束时,销毁ThreadLocal变量
LoginUtil.remove();
注意:
请求结束时一定要销毁ThreadLocal变量
因为ThreadLocal变量是跟线程绑定的,像springboot类型的项目,线程池是公用的,所以线程id是一样的,若没有及时销毁ThreadLocal变量,线程下次被重新分配后,可能会读取到上次与该线程绑定是ThreadLocal变量值,造成内存泄漏。
三:ThreadLocal的坑、ThreadLocal内存泄漏
3.1 内存泄漏示例
如下是一个内存泄漏示例:线程池中有5个线程,模拟10次请求,前五次请求set赋值,后五次请求get获取值。
/**
* ThreadLocal与内存泄漏
*
* 问题场景:项目中使用ThreadLocal保证单个请求线程中的局部线程安全,用户A成功登录后
* 将用户信息存储在ThreadLocal中,用户B登录失败,但是调用了ThreadLocal的get方法,
* 结果将用户A的信息获取到了。
*
* 结论:springboot项目中使用了线程池,用户A登录时分配线程1,用户信息保存在线程1的ThreadLocal中,
* 当用户A的请求完成后线程被回收,用户B登录时,也分配到了线程1,此时调用ThreadLocal方法会认为
* 用户A与用户B的线程是同一个,所以会读取到线程A的信息。
*
* 解决方法:每次请求完成后,需要手动调用ThreadLocal.remove()方法。
*
* 案例描述:创建一个线程池,里面有五个线程,模拟十个请求进入,当前五个请求执行完后,线程被回收,
* 重复分配给后五个请求。前五个请求中调用ThreadLocal.set方法存储一个对象,后五个请求调用Thread.get
* 方法取值,理论上是不应该取到值的,结果打印后发现取到了之前五个请求set的值。
*/
public class ThreadLocal_MemoryLeak {
public static final Integer SIZE = 10;
static ThreadPoolExecutor executor = new ThreadPoolExecutor(
5, 5, 1,
TimeUnit.MINUTES, new LinkedBlockingDeque<>());
static class LocalVariable {
private byte[] locla = new byte[1024*1024*10];
}
static final ThreadLocal<LocalVariable> local = new ThreadLocal<>();
static int num=0;
public static void main(String[] args) {
try {
for (int i = 0; i < SIZE; i++) {
num++;
executor.execute(() -> {
if(num<=5) {
local.set(new LocalVariable());
}
System.out.println(Thread.currentThread().getName()+":"+local.get());
// local.remove();
});
Thread.sleep(100);
}
executor.shutdown();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
以上代码会造成内存泄漏,理论上来说后五次请求应该get不到值才对,但是由于前五次请求set赋值后,请求结束后没有remove清除掉ThreadLocal的值,造成线程重新分配后,由于线程id相同,导致读取到了之前应该清除的值。
3.2 什么是内存泄漏
先回顾下什么叫内存泄漏,对应的什么叫内存溢出
①Memory overflow:内存溢出,没有足够的内存提供申请者使用。
②Memory leak:内存泄漏,程序申请内存后,无法释放已申请的内存空间,内存泄漏的堆积终将导致内存溢出。
显然是TreadLocal在不规范使用的情况下导致了内存没有释放。
3.3 springboot默认线程数
我是在一次项目实际使用中,遇到了ThreadLocal内存泄漏问题,多个请求访问时,后续的请求读取到了前面请求的threadLocal变量值,当时的项目是springboot的。默认使用tomcat链接池。
springboot默认tomcat的相关配置在依赖包org.springframework.boot.autoconfigure.web.ServerProperties.class文件中
下图可做参考
- acceptCount : 请求等待队列大小,当tomcat没有控线线程处理连接请求时,新的请求进入等待队列,默认为100,当超出acceptCount后,新的请求被拒绝
- maxConnections : tomcat能处理的最大并发连接数,超出进入等待队列(acceptCount控制),连接会等待,不能被处理
- minSpareThreads :线程池最小线程数,
默认为10
,该配置指定线程池可以维持的空闲线程数量 - maxThreads :线程池最大线程数,默认200,当线程池空闲后会释放,保留minSpareThreads数量
3.4 ThreadLocal内存泄漏解决方法
①:规范使用ThreadLocal变量,请求结束时及时调用remove方法回收当前线程分配的ThreadLocal变量。
②:ThreadLocal是跟当前线程绑定的,若线程id保证不重复也是一种解决方法。
参考文章:
https://blog.csdn.net/zzg1229059735/article/details/82715741
https://www.cnblogs.com/twoheads/p/9646415.html
https://www.cnblogs.com/xdcat/p/13051561.html