ThreadLocal讲解

kitten 发布于 2025-10-19 244 次阅读


今天来讲下 ThreadLocal 这个东西很常见,  在我们做Web 开发的时候,经常将一个请求线程相关的用户信息(如 UserId)存入 ThreadLocal,在后续的 Service、Dao 层都可以直接获取,无需在方法参数中层层传递。

使用场景

比如, 我们在定义Controller 的时候, 通过@RequestHeader 来获取请求头部的指定信息:

@PostMapping("/login")
public Response userLogin(@RequestHeader("userId") String userId) {
  log.info("===> 请求头的userId: {}", userId)
  ...
}

上面是一个简单的接口的定义。我们从请求头中获取 userId 这个属性, 从而进行后续业务处理, 不过这种方式就得在 所有需要从请求头中获取userId 的Controller中专门声明 @RequestHeader("userId")String userId 这样一段, 是不是太麻烦了? 所以我们可以通过 Filter+ThreadLocal来简化这个操作。

首先定义一下 ThreadLocal

package com.kitten.auth.filter;

import ...

public class LoginUserContextHolder {
// 初始化一个 ThreadLocal 变量
    private static final ThreadLocal<Map<String, Object>> LOGIN_USER_CONTEXT_THREAD_LOCAL
            = ThreadLocal.withInitial(HashMap::new);
    /**
     * 设置用户 ID
     *
     * @param value
     */
    public static void setUserId(Object value) {
        LOGIN_USER_CONTEXT_THREAD_LOCAL.get().put(GlobalConstants.USER_ID, value);
    }

    /**
     * 获取用户 ID
     * @return
     */
    public static Long getUserId() {
        Object value = LOGIN_USER_CONTEXT_THREAD_LOCAL.get().get(GlobalConstants.USER_ID);
        if (Objects.isNull(value)) {
            return null;
        }
        return Long.valueOf(value.toString());
    }
    /**
     * 删除 ThreadLocal
     */
    public static void remove() {
        LOGIN_USER_CONTEXT_THREAD_LOCAL.remove();
    }
}

在同服务下新建一个 HeaderUserId2ContextFilter :

package com.kitten.auth.filter;

import ...

@Component
@Slf4j
public class HeaderUserId2ContextFilter extends OncePerRequestFilter {

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
                                    FilterChain chain) throws ServletException, IOException {
        // 从请求头中获取用户 ID
        String userId = request.getHeader(GlobalConstants.USER_ID);

        // 判断请求头中是否存在用户 ID
        if (StringUtils.isBlank(userId)) {
            // 若为空,则直接放行
            chain.doFilter(request, response);
            return;
        }

        log.info("===== 设置 userId 到 ThreadLocal 中, 用户 ID: {}", userId);
        LoginUserContextHolder.setUserId(userId);

        try {
            chain.doFilter(request, response);
        } finally {
            // 一定要删除 ThreadLocal ,防止内存泄露
            LoginUserContextHolder.remove();
            log.info("===== 删除 ThreadLocal, userId: {}", userId);
        }
    }
}
  • HeaderUserId2ContextFilter 继承自 OncePerRequestFilter,确保每个请求只会执行一次过滤操;
  • request.getHeader(GlobalConstants.USER_ID); : 从请求头中获取用户 ID(userId);
  • 打印一行日志,看看等会测试的时候,能否拿的到请求头中的用户 ID;
  • chain.doFilter(request, response); : 将请求和响应传递给过滤链中的下一个过滤器。如果没有下一个过滤器,则请求会到达控制器。

这样就可以很方便的获取请求头传递的数据:

@PostMapping("/login")
public Response userLogin() {
  Long userId = LoginUserContextHolder.getUserId();
  log.info("===> 请求头的userId: {}", userId)
  ...
}

ThreadLocal 的使用场景就是当每个线程只想操作属于自己的变量的时候, 并且别的线程也访问不到。

介绍完如何使用, 下面来说说常见问题

核心原理

ThreadLocal 的原理可以概括为:以线程为 Key,以存储的变量为 Value 的一个特殊 Map

当我们创建一个 ThreadLocal 对象并调用 set 方法时,其实是在当前线程中初始化了一个 ThreadLocalMap。

// 在 java.lang.Thread 类中
ThreadLocal.ThreadLocalMap threadLocals = null;

ThreadLocalMap 是 ThreadLocal 的一个静态内部类,它实现了一个自定义的 Map 结构。这个 Map 的 Key 是 ThreadLocal 对象本身(的弱引用)Value 是你存入的变量值

再来说下工作流程,调用set()方法 时,会将 value 存入 ThreadLocalMap。

public void set(T value) {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null) {
        map.set(this, value);
    } else {
        createMap(t, value); // 第一次调用时创建 Map
    }
}

// 创建 ThreadLocalMap
void createMap(Thread t, T firstValue) {
    t.threadLocals = new ThreadLocalMap(this, firstValue);
}
  1. 获取当前正在执行的线程 Thread.currentThread()
  2. 获取该线程的 ThreadLocalMap 对象。
  3. 如果 Map 不为空,则以 this(即当前 myThreadLocal 对象) 为 Key,以你要存储的 value 为 Value,存入 Map。
  4. 如果 Map 为空,则先为当前线程创建这个 Map

调用get()方法时,会从 map 中获取以当前 ThreadLocal 为 key 的 Entry

public T get() {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t); // 获取当前线程的 ThreadLocalMap
    
    if (map != null) {
        // 从 map 中获取以当前 ThreadLocal 为 key 的 Entry
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
            @SuppressWarnings("unchecked")
            T result = (T)e.value;
            return result;
        }
    }
    // Map 不存在或找不到 Entry,调用初始化方法
    return setInitialValue();
}
  1. 获取当前正在执行的线程 Thread.currentThread()
  2. 获取该线程的 ThreadLocalMap 对象。
  3. 如果 Map 不为空,则以 this(即当前 myThreadLocal 对象) 为 Key,在 Map 中查找对应的 Entry,并返回其 Value。
  4. 如果 Map 为空或找不到对应的 Entry,则调用 initialValue 方法来初始化一个值并返回(默认返回 null,可通过重写 initialValue 来设置默认值)。

当我们使用 ThreadLocal时, 创建的过程为:

  • 首先在应用中, 我们创建两个 ThreadLocal
    ThreadLocal<Long> userIdThreadLocal = new ThreadLocal<>();
    ThreadLocal<Long> connectionThreadLocal = new ThreadLocal<>();
    
  • 假设有两个线程在工作, 那么对应的情况是:
    Thread-1 ──────> ThreadLocalMap-1
    ├─ Key: userIdThreadLocal (弱引用) ──> Value: userId_1
    └─ Key: connectionThreadLocal (弱引用) ─> Value: Connection_1
    Thread-2 ──────> ThreadLocalMap-2
    ├─ Key: userIdThreadLocal (弱引用) ──> Value: userId_2
    └─ Key: connectionThreadLocal (弱引用) ─> Value: Connection_2

从最开始就强调了一个概念 "弱引用", 这是啥?

Java 设计了不同强度的引用,主要是为了更灵活地控制对象的生命周期,方便 JVM 进行垃圾回收。

从强到弱大致分为四种:

  1. 强引用 (Strong Reference)
  2. 软引用 (Soft Reference)
  3. 弱引用 (Weak Reference)
  4. 虚引用 (Phantom Reference)

弱引用是用 java.lang.ref.WeakReference 类实现的引用。

  • 被弱引用关联的对象只能生存到下一次垃圾收集发生之前
  • 当垃圾收集器工作时,无论当前内存是否足够,都会回收掉只被弱引用关联的对象

那么 ThreadLocal使用了 WeakReference, 就是用于保证资源可以被释放,减少内存泄露。

内存泄露

线程被复用的场景下,ThreadLocal有内存泄露的风险。

前面讲到 ThreadLocalMap,本质上是一个 Entry[] 数组, 一个 Entry通过弱引用关联当前 ThreadLocal对象, 通过强引用 Value保存设置的值。

当ThreadLocal 作为成员变量的时候, 随着方法执行完毕, 相应的栈帧也出栈了, 那么 ThreadLocal -> ThreadLocal对象 这条强引用链也就没了(排除线程复用场景, 如定义线程池), 那么, 没有别的栈对ThreadLocal引用的话, ThreadLocal对象对应的Entry将无法被访问到, 此時value 一直指向某个强引用对象,那么这个对象就不会被回收,从而导致内存泄漏。

当ThreadLocal 作为静态变量的时候, 有静态变量这条强引用存在, ThreadlocalMap中的Key弱引用不会被GC回收, 正常情况下不会出现内存泄露, 但是当类加载器被卸载时, 对应的类会被回收, 那么这个强引用就没了

出现内存泄露的核心的点就是: 一个线程一直在运行,并且 value 一直指向某个强引用对象。

怎么解决呢?手动remove() 即可。

那为啥把Key 设置为弱引用? 反正都会内存泄露。其实, 它就是为了减少内存泄露的发生, 而且明显的当内存不足时, JVM能及时回收掉弱引用对象。

一旦 key 被回收,ThreadLocalMap 在进行 set、get 的时候就会对 key 为 null 的 Entry 进行清理(部分清理)。

set()

private void set(ThreadLocal key, Object value) {
            Entry[] tab = table;
            int len = tab.length;
            int i = key.threadLocalHashCode & (len-1);

            for (Entry e = tab[i];
                 e != null;
                 e = tab[i = nextIndex(i, len)]) {
                if (e.refersTo(key)) {
                    e.value = value;
                    return;
                }

                if (e.refersTo(null)) {
                    replaceStaleEntry(key, value, i);
                    return;
                }
            }

            tab[i] = new Entry(key, value);
            int sz = ++size;
            if (!cleanSomeSlots(i, sz) && sz >= threshold)
                rehash();
        }

replaceStaleEntry()

private void replaceStaleEntry(ThreadLocal key, Object value, int staleSlot) {
            Entry[] tab = table;
            int len = tab.length;
            Entry e;

            int slotToExpunge = staleSlot;
            for (int i = prevIndex(staleSlot, len); (e = tab[i]) != null; i = prevIndex(i, len))
                if (e.refersTo(null))
                    slotToExpunge = i;

            // Find either the key or trailing null slot of run, whichever
            // occurs first
            for (int i = nextIndex(staleSlot, len);
                 (e = tab[i]) != null;
                 i = nextIndex(i, len)) {
                // If we find key, then we need to swap it
                // with the stale entry to maintain hash table order.
                // The newly stale slot, or any other stale slot
                // encountered above it, can then be sent to expungeStaleEntry
                // to remove or rehash all of the other entries in run.
                if (e.refersTo(key)) {
                    e.value = value;

                    tab[i] = tab[staleSlot];
                    tab[staleSlot] = e;

                    // Start expunge at preceding stale entry if it exists
                    if (slotToExpunge == staleSlot)
                        slotToExpunge = i;
                    cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
                    return;
                }

                // If we didn't find stale entry on backward scan, the
                // first stale entry seen while scanning for key is the
                // first still present in the run.
                if (e.refersTo(null) && slotToExpunge == staleSlot)
                    slotToExpunge = i;
            }

            // If key not found, put new entry in stale slot
            tab[staleSlot].value = null;
            tab[staleSlot] = new Entry(key, value);

            // If there are any other stale entries in run, expunge them
            if (slotToExpunge != staleSlot)
                cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
        }

这种设计产生的内存泄露类型是明确的: key==null 的 entry, 而且有明确的解决方案remove。如果是强引用,那么threadlocal永远被 threadlocalMap引用即便在业务中不再使用, 尤其是在线程池这种线程长期存在的场景。

感谢大家的观看喵, 笔者还是小白一枚(/ω\), 哪里写的有问题欢迎联系笔者沟通 qq: 1948677720