ThreadLocal原理、ThreadLocal的使用、ThreadLocal内存泄漏

背景说明

鉴于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

  • 0
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值