ThreadLocal??

104 阅读3分钟

什么是 ThreadLocal?

ThreadLocal 叫做线程变量,使得 每个线程可以拥有自己的独立变量副本,底层在线程中维护了一个ThreadLocalMap,key就是各种的 ThreadLocal 对象,value可以是任意类型的值,可以是Long,User 等等

如图:

ThreadLocal 原理

set 方法:

public void set(T value) {
// 获取当前线程
    Thread t = Thread.currentThread();
// 获取当前线程的 ThreadLocalMap
    ThreadLocalMap map = getMap(t);
// map存在,将数据放入到map中
    if (map != null)
        map.set(this, value);
    else
        // 不存在 先创建map,在放入数据
        createMap(t, value);
}

get 方法:

public T get() {
    //获取当前线程
    Thread t = Thread.currentThread();
    //获取线程中的map
    ThreadLocalMap map = getMap(t);
    if (map != null) {
        //将自身当作key 获取值
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
            @SuppressWarnings("unchecked")
            T result = (T)e.value;
            return result;
        }
    }
    return setInitialValue();
}

我们从 set 和 get 方法中可以看出,ThreadLocal都是从自己的 Map 将数据 设置 和 取出来的,所以我们才说每个线程都有自己独立的变量副本,线程之间是隔离的。

ThreadLocal 内存泄漏问题

可以看到,ThreadLocal 本身并不存储对象,是调用 Thread 中的 ThreadLocalMap 来保存的,而 Thread 强引用 ThreadLocalMap 对象,如果 Thread 生命周期很长,不能及时地回收,就会导致 ThreadLocalMap对象里 Entry 地 Value 出现内存泄漏地可能

我们来看一下 ThreadLocalMap 长什么样:

从图中可以看到,里面有一个 Entry 数组,可以看到 Entry 继承了 WeakReference,注意了 Entry 不是弱引用,而是 Entry中的 key,也就是 ThreadLocal 对象 是弱引用,但是你别完了,Value 是一个强引用。

ThreadLocalMap 在设计的时候将key 设计成一个弱引用,当主动的调用 set / get / rehash 方法的时候,会将 key 为 null 的 value 进行回收,但是,如果线程是长期存活的,ThreadLocalMap 中的 Value 一直是强引用,那么就不会被垃圾回收,那么就没办法避免 Value 内存泄漏。

所以:真正导致内存泄露的主要原因是,线程强引用 ThreadLocalMap,如果线程一直存在 ThreadLocalMap 中 Entry 的 Value 这个强引用一直存在,没有被回收,才会导致内存泄漏。

实际开发的过程中使用完 ThreadLocal 后一定要手动的 remove !!

ThreadLocal 的应用

学习过 Java web 的都知道,我们一般在拦截器中使用 ThreadLocal 来保存用户的信息,以便下游可以获取到用户的信息。

示例例子:

用户上下文:

public class UserContext {
    private static final ThreadLocal<User> userHolder = new ThreadLocal<>();

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

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

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

SpringMvc 拦截器:

import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

@Component
public class UserInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // 假设从请求中获取用户信息
        User user = getUserFromRequest(request);
        UserContext.setUser(user); // 将用户信息存入 ThreadLocal
        return true;
    }

    private User getUserFromRequest(HttpServletRequest request) {
        // 在这里实现你的逻辑来获取用户信息
        // 例如,从请求头或Cookie中获取用户ID,并从数据库中查询用户信息
        return new User("exampleUser");
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        UserContext.clear(); // 清除 ThreadLocal 中的用户信息
    }
}

最后,我在拦截器的 afterCompletion 中 remove 掉了 ThreadLocal 避免 内存泄漏的风险。