ThreadLocal实践

背景说明
鉴于ThreadLocal的特性,ThreadLocal会给每个Thread线程单独分配一块变量副本,所以项目中很多地方会使用ThreadLocal进行处理,比如登录接口,每次请求进来后将操作员信息放到ThreadLocal中与当前线程进行绑定,当前线程任何时刻都能获取用户登录信息。

问题说明: 我在项目使用时用了拦截器,每次请求进来的时候获得当前用户的登录信息,并存放到ThreadLocal中,以便于逻辑处理时从ThreadLocal中获取当前登录用户。但是有一次遇到了一个问题,本次请求已经结束了,下一个请求进来的时候从ThreadLocal中读取到了上一个请求的登录用户信息。

问题分析: ThreadLocal是跟线程id进行绑定的,由于代码中忘记在拦截器中请求结束后清除ThreadLocal中的值,后面一次请求的线程id与之前是相同的,导致读取到了上一个请求的登录信息。

问题解决: 每次请求结束后,清除掉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获取值。

package com.hmblogs.backend.util;

import java.util.concurrent.LinkedBlockingDeque;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

/**
 * 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();
        }
    }

}

以上代码如果把local.remove()这行代码注释,会造成内存泄漏,执行结果如下:

理论上来说后五次请求应该get不到值才对,但是由于前五次请求set赋值后,请求结束后没有remove清除掉ThreadLocal的值,造成线程重新分配后,由于线程id相同,导致读取到了之前应该清除的值。

如果不注释local.remove()这行代码,执行结果如下:

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保证不重复也是一种解决方法。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值