巧用ThreadLocal,避免内存泄露

01 引言

在Java多线程编程中,ThreadLocal是一个强大却常被误解的工具。它能为每个线程创建变量的独立副本,避免线程间的数据干扰,堪称线程安全的“变量保险箱”。但若使用不当,它也可能成为内存泄露的隐形杀手。

之前在父子线程提到过ThreadLocal,本章将详细介绍其原理,使用以及如何避免被面试官长问的内存泄露问题。

02 实现原理

ThreadLocal用来保存线程的独有变量,保存在其内部的一个内部类(ThreadLocalMap)中,也是一种k-v结构。k就是当前的ThreadLocal对象,而v就是我们想要保存的值。

那现在有三个对象出现在我们面前:ThreadThreadLocalThreadLocalMap

他们之间是怎样的关系呢?我们先看看Thread类中的关键属性:

可以看出Thread中维护了两个ThreadLocal.ThreadLocalMap①②,其中②是被InheritableThreadLocal类维护的,专门为父子线程继承关系设计的,又继承ThreadLocal,所以和ThreadLocal的功能一致,本节不做不讨论。

而①正是被ThreadLocal维护的属性,所以我们就可以看到这样的关系:Thread维护了ThreadLocalMap成员变量,而ThreadLocalMap维护了一个弱引用的Entry内部类,而Entry则以ThreadLocal作为key,维护着我们需要需要保存的value。大致如图:

ThreadLocalMap的构造类似一个HashMap的结构,可以直接看做一个Map

03 使用场景

ThreadLocal提供了一种线程级别的数据存储机制,每一个线程都有自己独立的ThreadLocal副本,每个线程就可以独立的操作这些数据信息,并不会对其他线程产生影响。

3.1 会话管理

很多应用中需要用户登录鉴权,校验通过之后就会把用户登录的信息存储在ThreadLocal中,这样后续的流程就可以直接从ThreadLocal中获取就好了。

public class UserContextHolder {
    private static final ThreadLocal<User> currentUser = new ThreadLocal<>();
    
    public static void set(User user) {
        currentUser.set(user);
    }
    
    public static User get() {
        return currentUser.get();
    }
    
    public static void clear() {
        currentUser.remove(); // 关键清理!
    }
}

类似上面的代码一般会作为一个工具类,用户登录成功之后会调用set()方法保存用户信息,获取时就是调用get()方法。直到当前线程结束,则需要调用clear()方法清理数据。

登录通过时,通常在拦截器里面被调用:

public class LoginInterceptor extends HandlerInterceptorAdapter {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        
        // 登录校验......
        User user = ...
            
		// 保存登录信息
        UserContextHolder.set(user);
        return super.preHandle(request, response, handler);
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        // 调用完成之后清理数据
        UserContextHolder.clear();
    }
}

3.2 线程安全

ThreadLocal本身就是线程安全的,可以用来存储一个线程不安全的类,来保证其线程安全。如SimpleDateFormat

private static ThreadLocal<SimpleDateFormat> dateFormat = 
    ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd"));

// 每个线程获取自己的SimpleDateFormat实例,避免多线程竞争
String date = dateFormat.get().format(new Date());

3.3 数据库事务管理

Spring框架使用ThreadLocal保存当前线程的事务连接,确保同一事务中的所有操作使用同一个Connection,包括多数据源的切换。

3.4 链路追踪TraceId

traceId经常被用在链路追踪的日志中,金记录每次请求的未必表示,通常也被记录在ThreadLocal中。

04 内存泄露

ThreadLocal的内存泄露经常被面试官问到,算是一道经典的面试题。ThreadLocal已经尽可能的帮我们解决了一部分问题,比如引入弱引用。但是依然还有有内存泄露的风险,需要我们开着自己解决。

解决的方式也很简单,直接使用ThreadLocal中的remove()方法即可。

4.1 内存泄露发生的过程

导致内存泄露部分就是其在堆上存储的ThreadLocalMap中的K-V部分。

  • ThreadLocal对象失去强引用(如设置为null)时,由于Key是弱引用(继承WeakReference<ThreadLocal<?>>),下次GCKey会被回收,Entry变为Key=null, Value=原值
  • Value仍被Entry强引用,而Entry又被ThreadLocalMap强引用,Map又被Thread(常驻线程池)强引用。
  • 若未调用remove()Value会一直占用内存,导致泄露。

4.2 线程池加剧泄露

线程池的创建一般都是指定核心线程,而核心线程不会不会随着任务的结束而结束,ThreadLocal也不会被销毁。再加上其他线程的ThreadLocal,更容易造成内存的泄露。

05 小结

ThreadLocal如一把双刃剑:合理使用可优雅解决线程数据隔离问题,疏于管理则会导致内存泄露。牢记 “用后即焚” 原则,在finally中调用remove(),方能发挥其威力而不被反噬。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

智_永无止境

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值