ThreadLocal源码解析

一. ThreadLocal简介

在日常的 Java 开发中,我们经常会遇到这样一类问题:如何在不增加方法参数传递成本的情况下,在同一个线程的多个方法调用之间共享数据?例如,请求链路中的用户信息、数据库连接、事务上下文等,这些数据本质上是"与线程绑定"的,但又不适合通过全局共享变量来管理,因为全局变量在多线程环境下会引发并发安全问题。这个时候,ThreadLocal 就成为了一种非常优雅且高效的解决方案。

ThreadLocal 的核心作用可以用一句话概括:为每个线程提供一份独立的变量副本,实现线程之间的数据隔离 。与我们平时使用的共享变量不同,ThreadLocal 并不是让多个线程去竞争同一份数据,而是让每个线程都拥有属于自己的"私有变量",从而避免了加锁、竞争等并发问题。这种设计本质上是一种"用空间换时间"的思想,通过牺牲一定的内存来换取更高的执行效率和更简单的编程模型。

在实际业务开发中,ThreadLocal 的使用场景非常广泛。例如,在 Web 请求处理中,我们可以通过 ThreadLocal 保存当前登录用户的信息,这样在整个请求处理链路中,无需层层传递参数就可以随时获取用户上下文;在数据库访问中,可以将 Connection 绑定到当前线程,实现同一线程内的事务一致性;在一些框架(如 Spring、MyBatis)中,ThreadLocal 更是被大量用于管理线程级别的资源和上下文信息。可以说,ThreadLocal 是构建"线程上下文(Thread Context)"的核心工具之一。

二. ThreadLocal使用示例

ThreadLocal的使用简单示例:

java 复制代码
```java
/**
 * 线程上下文(ThreadLocal 实现)
 *
 * 用于在同一个线程内共享请求级数据,例如用户信息、请求ID等
 */
public final class ThreadContext {

    /**
     * 每个线程独享一份 RequestContext
     */
    private static final ThreadLocal<RequestContext> CONTEXT =
            ThreadLocal.withInitial(RequestContext::new);

    private ThreadContext() {
    }

    /**
     * 获取当前线程的上下文对象
     */
    public static RequestContext getContext() {
        return CONTEXT.get();
    }

    /**
     * 设置用户信息
     */
    public static void setUserInfo(UserInfo userInfo) {
        CONTEXT.get().setUserInfo(userInfo);
    }

    /**
     * 获取用户信息
     */
    public static UserInfo getUserInfo() {
        return CONTEXT.get().getUserInfo();
    }

    /**
     * 设置请求ID
     */
    public static void setRequestId(String requestId) {
        CONTEXT.get().setRequestId(requestId);
    }

    /**
     * 获取请求ID
     */
    public static String getRequestId() {
        return CONTEXT.get().getRequestId();
    }

    /**
     * 清理 ThreadLocal(必须在请求结束时调用)
     */
    public static void remove() {
        CONTEXT.remove();
    }
}


/**
 * 请求上下文对象
 *
 * 存储与当前请求相关的数据(线程隔离)
 */
public class RequestContext {

    /**
     * 用户信息
     */
    private UserInfo userInfo;

    /**
     * 请求ID
     */
    private String requestId;

    // ===== getter / setter =====

    public UserInfo getUserInfo() {
        return userInfo;
    }

    public void setUserInfo(UserInfo userInfo) {
        this.userInfo = userInfo;
    }

    public String getRequestId() {
        return requestId;
    }

    public void setRequestId(String requestId) {
        this.requestId = requestId;
    }
}

三. 源码解析

1. get() 的核心源码

java 复制代码
/**
 * 获取当前线程中与这个 ThreadLocal 关联的值
 */
public T get() {
    // 1. 先拿到当前线程对象
    Thread t = Thread.currentThread();

    // 2. 再从当前线程中取出 ThreadLocalMap
    ThreadLocalMap map = getMap(t);

    // 3. 如果当前线程已经有 ThreadLocalMap,则尝试根据 this 作为 key 查找
    if (map != null) {
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
            @SuppressWarnings("unchecked")
            T result = (T) e.value;
            return result;
        }
    }

    // 4. 如果当前线程还没有 map,或者 map 中没有当前 key,
    //    就进入初始化流程
    return setInitialValue();
}

/**
 * 获取某个线程内部持有的 ThreadLocalMap
 *
 * 注意:
 * ThreadLocalMap 是挂在线程对象 Thread 上的,
 * 不是挂在 ThreadLocal 自己身上的。
 */
ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
}

/**
 * 当前线程第一次访问该 ThreadLocal 时,会走这个逻辑
 */
private T setInitialValue() {
    T value = initialValue();
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);

    if (map != null) {
        map.set(this, value);
    } else {
        createMap(t, value);
    }

    return value;
}

/**
 * 当线程第一次使用 ThreadLocal,并且 threadLocals 还不存在时,
 * 创建一个新的 ThreadLocalMap
 */
void createMap(Thread t, T firstValue) {
    t.threadLocals = new ThreadLocalMap(this, firstValue);
}

2. set() 的核心源码

java 复制代码
/**
 * 给当前线程设置一个与该 ThreadLocal 关联的值
 */
public void set(T value) {
    // 1. 获取当前线程
    Thread t = Thread.currentThread();

    // 2. 获取当前线程内部的 ThreadLocalMap
    ThreadLocalMap map = getMap(t);

    if (map != null) {
        // 3. 如果 map 已存在,则直接放入
        map.set(this, value);
    } else {
        // 4. 如果 map 不存在,则为当前线程创建一个新的 map
        createMap(t, value);
    }
}

也就是说,ThreadLocal 并没有"自己保存一份值",而是把值交给当前线程去保存。

因此,不同线程调用同一个 ThreadLocal.set(),最终写入的是各自线程内部的独立数据,互不影响。

java 复制代码
public class Thread implements Runnable {

    /**
     * 当前线程持有的 ThreadLocalMap
     *
     * 这个字段专门存放普通 ThreadLocal 的数据。
     * 每个线程对象都有自己独立的一份。
     */
    ThreadLocal.ThreadLocalMap threadLocals = null;

    /**
     * 这个字段用于 InheritableThreadLocal
     *
     * 父线程创建子线程时,可以把值传递给子线程。
     */
    ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
}

很多人刚接触 ThreadLocal 时会误以为:ThreadLocal 内部维护了一个 Mapkey 是线程,value 是数据。

其实正好相反。真实结构是:每个线程内部维护一个 ThreadLocalMapkeyThreadLocalvalue 是线程私有数据 。这也是为什么 ThreadLocal 能做到线程隔离的根本原因。

3. ThreadLocalMap 的 Entry 结构

java 复制代码
static class ThreadLocalMap {

    /**
     * Entry 继承 WeakReference
     *
     * 也就是说:
     * key(ThreadLocal)是弱引用
     * value(真正保存的数据)是强引用
     */
    static class Entry extends WeakReference<ThreadLocal<?>> {

        /**
         * 与 ThreadLocal 关联的实际值
         */
        Object value;

        Entry(ThreadLocal<?> k, Object v) {
            super(k);   // 弱引用保存 key
            value = v;  // 强引用保存 value
        }
    }
}

ThreadLocalMap.Entry 并不是普通的键值对,而是做了一个特殊设计:

  • keyWeakReference<ThreadLocal<?>>
  • value:普通强引用对象

也就是说,key 是弱引用,value 是强引用。

这样设计的目的,是为了避免某个 ThreadLocal 对象已经不用了,却因为还被 ThreadLocalMap 强引用着而无法回收。

但这也带来了一个副作用:如果 ThreadLocal 被回收了,而线程还活着,那么 Entry 就可能变成:

java 复制代码
key = null
value = 业务对象

这就是所谓的"脏 Entry",如果不及时清理,就可能造成内存长期占用。

4. ThreadLocalMap 的底层数组结构

java 复制代码
static class ThreadLocalMap {

    /**
     * 初始容量,必须是 2 的幂
     */
    private static final int INITIAL_CAPACITY = 16;

    /**
     * 真正存储数据的数组
     *
     * 每个元素都是一个 Entry:
     * key   = ThreadLocal(弱引用)
     * value = 线程私有数据
     */
    private Entry[] table;

    /**
     * 当前数组中已使用的 Entry 数量
     */
    private int size = 0;

    /**
     * 扩容阈值
     */
    private int threshold;

    /**
     * 创建 ThreadLocalMap,并把第一条数据直接放进去
     */
    ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
        table = new Entry[INITIAL_CAPACITY];

        // 计算数组下标
        int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);

        // 存入第一条 Entry
        table[i] = new Entry(firstKey, firstValue);

        size = 1;

        // 设置扩容阈值
        threshold = setThreshold(INITIAL_CAPACITY);
    }
}

ThreadLocalMap 的底层不是 HashMap,而是一个定制化的数组结构。它使用数组来保存 Entry,并通过 threadLocalHashCode & (len - 1) 的方式定位下标。由于数组长度总是 2 的幂,因此这种位运算定位效率很高。同时,ThreadLocalMap 没有采用链表法解决冲突,而是采用了开放地址法,也就是冲突后继续往后找空位。

这样做的好处是:

  • 结构更紧凑
  • 减少链表节点带来的额外对象和指针跳转
  • 更有利于 CPU cache 命中
  • ThreadLocal 这种典型小规模存储场景更加高效

5. ThreadLocalMap 的 getEntry():如何查找元素

java 复制代码
static class ThreadLocalMap {

    /**
     * 根据 ThreadLocal key 获取对应 Entry
     */
    private Entry getEntry(ThreadLocal<?> key) {
        // 先根据 hash 直接定位一个下标
        int i = key.threadLocalHashCode & (table.length - 1);

        Entry e = table[i];

        // 如果正好命中,直接返回
        if (e != null && e.get() == key) {
            return e;
        }

        // 如果没命中,进入线性探测
        return getEntryAfterMiss(key, i, e);
    }

    /**
     * 发生哈希冲突后,继续向后查找
     */
    private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
        Entry[] tab = table;
        int len = tab.length;

        while (e != null) {
            ThreadLocal<?> k = e.get();

            // 找到了对应的 key
            if (k == key) {
                return e;
            }

            // 如果发现 key 已经被 GC 回收了,则顺便清理脏数据
            if (k == null) {
                expungeStaleEntry(i);
            } else {
                // 继续向后找,下标到头后回环到 0
                i = nextIndex(i, len);
            }

            e = tab[i];
        }

        return null;
    }

    /**
     * 线性探测:下一个下标
     */
    private static int nextIndex(int i, int len) {
        return ((i + 1 < len) ? i + 1 : 0);
    }
}

ThreadLocalMap 查找元素时,先根据 hash 值定位数组下标。如果当前位置不是目标 key,就说明发生了冲突,这时不会像 HashMap 那样走链表,而是采用线性探测:

  • 从当前位置往后逐个查找
  • 数组到尾后回到头部继续找
  • 直到找到目标 key 或遇到空槽位为止

同时,如果在线性探测过程中发现某个 Entrykey 已经变成 null,说明这个 ThreadLocal 已被 GC 回收,此时还会顺带触发脏数据清理。
ThreadLocalMap 没有使用 HashMap 的链表或红黑树结构,而是采用了数组加开放地址法(线性探测)。这种设计不会导致查找错误,因为插入和查找都遵循同样的探测路径:从 hash 位置开始,逐步向后查找,直到找到目标 key 或空槽位。虽然理论上可能退化为线性扫描,但 ThreadLocalMap 的数据量通常很小,加上使用黄金分割 hash 使分布均匀,因此冲突概率很低。

相比 HashMap,这种结构的优势在于:

  • 使用连续内存,CPU cache 友好
  • 没有链表指针,减少内存访问跳跃
  • 对象数量更少,降低 GC 压力

因此它更适合 ThreadLocal 这种"小数据、高频访问"的场景,而不是通用的大规模数据存储。

6. ThreadLocalMap 的 set():如何插入元素

java 复制代码
static class ThreadLocalMap {

    /**
     * 向当前线程的 ThreadLocalMap 中设置值
     */
    private void set(ThreadLocal<?> key, Object value) {
        Entry[] tab = table;
        int len = tab.length;

        // 先根据 key 的 hash 定位下标
        int i = key.threadLocalHashCode & (len - 1);

        // 使用开放地址法处理冲突
        for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) {
            ThreadLocal<?> k = e.get();

            // 如果 key 已存在,则直接覆盖 value
            if (k == key) {
                e.value = value;
                return;
            }

            // 如果发现某个脏 Entry(key 已被 GC 回收),
            // 则进入替换脏槽位的逻辑
            if (k == null) {
                replaceStaleEntry(key, value, i);
                return;
            }
        }

        // 找到空槽位,直接插入
        tab[i] = new Entry(key, value);
        int sz = ++size;

        // 插入后尝试清理脏 Entry;如果没清理掉太多且容量达到阈值,则扩容
        if (!cleanSomeSlots(i, sz) && sz >= threshold) {
            rehash();
        }
    }
}

插入过程可以概括为三种情况:

  • 第一种,如果当前 key 已经存在,那么直接覆盖旧值。
  • 第二种,如果探测过程中发现某个槽位的 key 已经是 null,说明这里存在脏 Entry,此时不会简单跳过,而是触发专门的替换和清理逻辑。
  • 第三种,如果一路探测后找到了空槽位,就直接插入新的 Entry。

这说明 ThreadLocalMap 在写入时,不仅做插入,还会顺带做一部分垃圾清理工作。这也是它"懒清理"设计的一部分。

四. 为什么ThreadLocalMap的key为什么要使用弱引用

ThreadLocalMapkey 使用弱引用,是为了避免 ThreadLocal 对象失去外部引用后,仍然被 Thread 持有,导致无法被 GC 回收,从而产生内存泄漏。

java 复制代码
			ThreadLocal
		        │
		        │(作为 key)
		        │
		        │
		        │
Thread(线程)	↓
  └── ThreadLocalMap
         └── Entry[]
               ├── key   = ThreadLocal(弱引用)
               └── value = 业务数据(强引用)

上面是ThreadLocal的结构关系图,从线程角度看应该是:

java 复制代码
ThreadLocal(入口)
        │
        ├───────────────────────────┐
        │               			│
        ↓              				↓
     线程A               		   线程B
     │                  			│
     ↓                  			↓
ThreadLocalMap    				ThreadLocalMap
     │                  			│
     ├── Entry          			├── Entry
     │     key = ThreadLocal     	│     key = ThreadLocal
     │     value = A    			│     value = B

从上面的结构中可以看出:

我们常用的ThreadLocal可以是:static final的,也可以是我们随意创建的,但是:

  • Thread 强引用 ThreadLocalMap
  • ThreadLocalMap 强引用 Entry
  • Entry
    key = 弱引用 ThreadLocal
    value = 强引用对象

如果ThreadLocal的key是强引用,当有如下代码:

java 复制代码
ThreadLocal tl = new ThreadLocal();
tl.set(new BigObject());

// 这里把引用断掉
tl = null;

这个时候GC回收不了创建的这个ThreadLocal tl,原因就是引用关系是:Thread → ThreadLocalMap → Entry → key(ThreadLocal)

所以Entrykey被设计成了弱引用,当是弱引用时,GC回收时ThreadLocal就可以被回收了,Entry.key → 变成 null,同时结构变成:Entry(null, value),所以就会有新的问题:value 还在 。因为value是强引用关系是:Thread → ThreadLocalMap → Entry → value

所以我们在使用ThreadLocal的时候一定要注意及时清理,如果不调用 remove,仍然可能出现 value 无法释放的问题。因此 ThreadLocal 采用的是"弱引用 + 懒清理"的策略,并要求开发者在使用完后手动调用 remove,特别是在使用线程池时(线程复用)。

那么为啥不把value设置成弱引用呢?因为弱引用在一次GC之后会被回收,所以value不能是弱引用,那么这个时候你会不会想到:那为啥key作为弱引用就不GC回收呢?

原因是我们使用的时候,创建的ThreadLocal都是在代码中有引用的,或者像文章开头的例子中一样,使用static 修饰,这个时候我们创建的ThreadLocal引用是一直存在的,所以ThreadLocal作为弱引用的key不会担心被回收。

五. 总结

ThreadLocal 的设计本质上并不是"一个全局容器按线程存值",而是"每个线程内部维护一个私有 Map"。

其中,ThreadLocal 自身只负责提供访问入口和 key,真正的数据存储在线程对象的 ThreadLocalMap 中。
ThreadLocalMap 又不是普通的 HashMap,而是一个基于数组和开放地址法实现的定制结构,它通过特殊的 hash 递增策略降低冲突,并在 getsetremove 过程中顺带清理脏 Entry

同时,由于 Entrykey 是弱引用、value 是强引用,因此如果不及时 remove(),在线程长期存活的场景下就可能留下 value 无法及时释放的问题,这也是 ThreadLocal 使用中最经典的风险点。

相关推荐
qq_232045579 天前
精积微半导体面试(部分)
netty·策略模式·nio·内存抖动·threadlocal·bitmap·复用
无心水11 天前
【常见错误】1、Java并发工具类四大坑:从ThreadLocal到ConcurrentHashMap,你踩过几个?
java·开发语言·后端·架构·threadlocal·concurrent·java并发四大坑
weisian1511 个月前
JVM--13-深入ThreadLocal:线程私有数据的隔离艺术与实战陷阱
开发语言·jvm·threadlocal
C雨后彩虹1 个月前
ThreadLocal全面总结,从理论到实践再到面试高频题
java·面试·多线程·同步·异步·threadlocal
C雨后彩虹1 个月前
跨线程数据传递InheritableThreadLocal的原理
java·多线程·同步·异步·threadlocal
专业的小学生2 个月前
单线程缓存
缓存·线程·thread·threadlocal
袁慎建@ThoughtWorks2 个月前
ThreadLocal那些事儿
java·jdk·多线程·threadlocal
zfj3212 个月前
从源码层面解析一下ThreadLocal的工作原理
java·开发语言·threadlocal
J_liaty3 个月前
ThreadLocal 深度解析:原理、实战与避坑指南
java·spring·threadlocal