ThreadLocal学习

ThreadLocal学习

开心 404 2020-06-05

ThreadLocal 将一个变量隔离在某一个线程上,即该变量只在某个线程本身可见,其他线程无法访问,可以实现线程的安全性,而且可以避免线程同步带来的性能损失

image.png

如图所示,每个 Thread对象中都持有一个 ThreadLocalMap 成员变量,每个 ThreadLocalMap 有一个键值对 Entry[] table,可以认为是一个 map,ThreadLocal 就是这个键,我们需要存储的数据做为值

1. 使用场景

1.1 每个线程需要一个独享的对象
  • 通常是工具类,典型需要使用的有 SimpleDateFormat 和 Random
  • Java8 可以使用线程安全的 DateTimeFormatter 和 ThreadLocalRandom
class DateUtil {

 private static final String DATE_FORMAT = "yyyy-MM-dd HH:mm:ss";

    public static ThreadLocal<SimpleDateFormat> dateFormat = new ThreadLocal<SimpleDateFormat>() {
        
        @Override
        protected SimpleDateFormat initialValue() {
            return new SimpleDateFormat(DATE_FORMAT);
        }
    };
}
1.2 每个线程内需要保存全局变量
  • 例如在拦截器中获取用户信息,可以直接让不同方法直接使用,避免参数传递的麻烦
public class ThreadLocalHolder {

    private static final ThreadLocal<User> userHolder = new ThreadLocal<>();

    public static void set(User user) {
        userHolder.set(user);
    }

    public static User get() {
        return userHolder.get();
    }

    public static Integer getId() {
        return userHolder.get().getId();
    }

    public static void remove() {
        userHolder.remove();
    }
}

public class ThreadLocalInterceptor implements HandlerInterceptor {

    @Autowired
    private RedisService redisService;

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
        throws Exception {

        String loginToken = request.getHeader(Const.HEAD_TOKEN);
        if (StringUtils.isBlank(loginToken)) {
            return false;
        }
        loginToken = Const.TOKEN_PREFIX + loginToken;
        String userJsonStr = redisService.get(loginToken);
        User user = JsonUtil.string2Obj(userJsonStr, User.class);
        if (user == null) {
            return false;
        }
        // 重置Session时间
        redisService.expire(loginToken, Const.REDIS_SESSION_EXPIRE_TIME);
        // 设置ThreadLocal
        ThreadLocalHolder.set(user);
        return true;
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,
        ModelAndView modelAndView) throws Exception {}

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex)
        throws Exception {
        ThreadLocalHolder.remove();
    }
}

2. 带来的好处

  • 达到线程安全
  • 不需要加锁,提高执行效率
  • 更高效的利用内存,节省开销
  • 免去传参的繁琐

3. 主要方法介绍

  • initialValue:初始化,该方法会返回当前线程对应的“初始值”,这是一个延迟加载的方法,只有在调用 get() 的时候才会触发,当线程第一次使用 get() 访问变量时,将调用此方法,除非线程之前调用了 set(),在这种情况下,不会为线程调用 initialValue(),通常,每个线程最多调用一次此方法,但如果已经调用了 remove() 后,再调用 get() ,则可能会再次调用此方法,如果不重写此方法,这个方法会返回 null,一般使用匿名内部类的方法来重写 initialValue(),以便在后续使用中可以初始化副本对象

  • set:为当前线程副本设置一个新值,set() 和 setInitialValue() 最后都是利用 map.set() 来设置值

  • get:得到当前线程副本对应的值,如果是首次调用 get(),则会调用 initialize 来得到这个值

  • remove:删除对应这个线程的值

4. 内存泄露问题

static class Entry extends WeakReference<ThreadLocal<?>> {
    /** The value associated with this ThreadLocal. */
    Object value;

    Entry(ThreadLocal<?> k, Object v) {
        super(k);
        value = v;
    }
}
4.1 value 的泄露

ThreadLocalMap 中的每一个 Entry 都是对 key 的弱引用(弱引用的特点是,如果这个对象只被弱引用关联,那么这个对象就可以被回收),同时每个 Entry 都包含了一个对 vluae 的强引用,正常情况下,当线程终止,保存在 ThreadLocal 里的 value 会被垃圾回收,因为没有任何强引用了,但是,如果线程不终止(比如线程需要保持很久),那么 key 对应的 value 就不能被回收,因为有以下的调用链:Thread -> ThreadLocalMap -> Entry(key 为null) -> value,因为 value 和 Thread 之间还存在这个强引用链路,所以导致 value 无法回收,就可能会出现 OOM,为了解决这个问题,JDK 会在 set,remove,rehash 方法中扫描 key 为 null 的 Entry,并把对应的 value 设置为 null,这样 value 对象就会被回收,但是如果一个 TreadLocal 不被使用,那么实际上 set,remove,rehash 方法也不会被调用,如果同时线程又不停止,那么调用链就会一直存在,那么就导致了 value 的内存泄露

4.2 如何避免

调用 remove 方法,就会删除对应的 Entry 对象,可以避免内存泄露,所以使用完 ThreadLocal 之后,应该调用 remove 方法


# java