Java并发编程Bug:ThreadLocal已用完但未清除


在Java中,有许多技术可以确保线程安全。你可以使用synchronized和Lock等关键字来锁定代码块。
但它们有一个共同的特点,那就是锁定会对代码的性能产生一定的损失。

其实,JDK中还提供了另一种思路,即:以空间换时间。
没错,使用ThreadLocal类就是这种思想的具体体现。

ThreadLocal为每个使用该变量的线程提供了一个独立的副本,这样每个线程就可以独立地改变自己的副本而不影响其他线程的相应副本。
ThreadLocal的用法大致是这样的:

  1. 首先创建一个CurrentUser类,里面包含了ThreadLocal的逻辑。

public class CurrentUser {
    private static final ThreadLocal<UserInfo> THREA_LOCAL = new ThreadLocal();
    
    public static void set(UserInfo userInfo) {
        THREA_LOCAL.set(userInfo);
    }
    
    public static UserInfo get() {
       THREA_LOCAL.get();
    }
    
    public static void remove() {
       THREA_LOCAL.remove();
    }
}

  • 在业务代码中调用CurrentUser类。

public void doSamething(UserDto userDto) {
   UserInfo userInfo = convert(userDto);
   CurrentUser.set(userInfo);
   ...

   // Business code
   UserInfo userInfo = CurrentUser.get();
   ...
}

在业务代码的第一行,将userInfo对象设置为CurrentUser,这样在业务代码中,userInfo就可以通过 获取刚刚设置的对象CurrentUser.get()。

尤其是在业务代码调用层次比较深的时候,这种用法非常有用,可以减少很多不必要的参数传递。
但是在高并发的场景下,这段代码就有问题了。数据只存储在 中ThreadLocal,数据用完后不及时清理。

即使ThreadLocal使用弱引用WeakReference,也可能存在内存泄漏问题,因为在 entry 对象中只有 key 设置为弱引用,但value没有。

那么,如何解决这个问题呢?

解决方案非常简单,只需在finally块中调用remove方法来清理无用的数据。

public void doSamething(UserDto userDto) {
   UserInfo userInfo = convert(userDto);
   
   try{
     CurrentUser.set(userInfo);
     ...
     
     // Business code
     UserInfo userInfo = CurrentUser.get();
     ...
   } finally {
      CurrentUser.remove();
   }
}