Skip to content

ThreadLocal 核心概念

「一个变量,让每个线程都觉得自己是独享的。」

这不是什么魔法,而是 ThreadLocal 的核心能力。

在 Web 开发中,你一定见过这样的代码:

java
// 在 Controller 设置用户信息
currentUser.set(user);

// 在 Service 层获取
User user = currentUser.get();

// 在 DAO 层还能获取到同一个用户
User user = currentUser.get();  // 依然是同一个

整个调用链没有任何显式的参数传递,但每层都能拿到当前请求的用户信息。这就是 ThreadLocal 的威力。

为什么需要 ThreadLocal

多线程环境下,传统的数据传递方式有两种:

方式一:方法参数传递

java
void controller(User user) {
    service.process(user);
}

void service(User user) {
    dao.save(user);
}

void dao(User user) {
    // 每层都要接收和传递
}

缺点:每个方法都要加参数,层层穿透,代码臃肿。

方式二:共享变量

java
private static User currentUser;  // 所有线程共享

void controller() {
    currentUser = getUserFromRequest();
    service.process();  // 不传参了
}

缺点:线程 A 设置的用户,被线程 B 读取到了,数据混乱。

ThreadLocal 的出现,就是为了解决这个两难问题。

ThreadLocal 的本质

ThreadLocal 为每个线程提供独立的变量副本:

┌──────────────────────────────────────────────────────────┐
│                    ThreadLocal 工作原理                   │
├──────────────────────────────────────────────────────────┤
│                                                          │
│  ThreadLocal           Thread                             │
│  ┌────────────┐      ┌──────────────────────────┐        │
│  │ value      │◄────►│ ThreadLocalMap           │        │
│  └────────────┘      │  ┌────────────────────┐  │        │
│                      │  │ key → ThreadLocal  │  │        │
│                      │  │ value → 线程独享值  │  │        │
│                      │  └────────────────────┘  │        │
│                      └──────────────────────────┘        │
│                                                          │
│  核心:值存储在 Thread 对象中,ThreadLocal 只是访问入口    │
│                                                          │
└──────────────────────────────────────────────────────────┘

关键点:ThreadLocal 本身不存储值,它只是一个访问入口。真正的值存储在每个线程自己的 ThreadLocalMap 中。

基本用法

java
public class ThreadLocalBasicDemo {

    private static final ThreadLocal<String> context = new ThreadLocal<>();

    public static void main(String[] args) {
        // 设置值
        context.set("主线程的数据");

        System.out.println("主线程: " + context.get());

        // 新线程看不到主线程的值
        Thread child = new Thread(() -> {
            System.out.println("子线程读取: " + context.get()); // null

            context.set("子线程的数据");
            System.out.println("子线程设置后: " + context.get());
        });

        child.start();
        child.join();

        // 主线程的值不受影响
        System.out.println("主线程最终: " + context.get());
    }
}

输出:

主线程: 主线程的数据
子线程读取: null
子线程设置后: 子线程的数据
主线程最终: 主线程的数据

两个线程互不干扰,各自有各自的副本。

带初始值的 ThreadLocal

每次 get() 都判断 null 很麻烦?可以设置初始值:

java
public class ThreadLocalWithInitial {

    // 方式1:匿名内部类重写 initialValue()
    private static final ThreadLocal<List&lt;String&gt;&gt; listLocal =
        new ThreadLocal&lt;List&lt;String&gt;&gt;() {
            @Override
            protected List&lt;String&gt; initialValue() {
                return new ArrayList&lt;&gt;();
            }
        };

    // 方式2:withInitial() (JDK 8+,更简洁)
    private static final ThreadLocal&lt;Integer&gt; intLocal =
        ThreadLocal.withInitial(() -&gt; 0);

    public static void main(String[] args) {
        // 无需手动 set,直接 get 就有初始值
        System.out.println(listLocal.get()); // []
        System.out.println(intLocal.get());  // 0

        // 使用
        listLocal.get().add("item");
        System.out.println(listLocal.get()); // [item]

        // 新线程依然是初始值
        new Thread(() -&gt;
            System.out.println("新线程 listLocal: " + listLocal.get())  // []
        ).start();
    }
}

ThreadLocal vs 共享变量

特性普通静态变量ThreadLocal
访问所有线程共享线程独享
同步需求需要 synchronized不需要
生命周期对象生命周期线程生命周期
典型用途共享数据线程上下文

典型使用场景

场景一:用户上下文传递

java
public class UserContextHolder {

    private static final ThreadLocal&lt;User&gt; currentUser = new ThreadLocal&lt;&gt;();

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

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

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

// Controller
public void handleRequest(HttpServletRequest request) {
    User user = authenticate(request);
    UserContextHolder.setUser(user);  // 设置
    try {
        doSomething();
    } finally {
        UserContextHolder.clear();  // 清理
    }
}

// Service(无需传参)
public void doSomething() {
    User user = UserContextHolder.getUser();  // 直接获取
    // ...
}

场景二:日期格式化(线程安全)

SimpleDateFormat 不是线程安全的,传统做法是每次调用都 new 一个。ThreadLocal 可以让它被复用:

java
public class DateFormatter {

    private static final ThreadLocal&lt;SimpleDateFormat&gt; dateFormat =
        ThreadLocal.withInitial(() -&gt;
            new SimpleDateFormat("yyyy-MM-dd"));

    public static String format(Date date) {
        return dateFormat.get().format(date);
    }
}

每个线程用自己的 SimpleDateFormat,既线程安全,又避免了重复创建。

场景三:数据库连接(事务管理)

java
public class TransactionManager {

    private static final ThreadLocal&lt;Connection&gt; connection =
        new ThreadLocal&lt;&gt;();

    public static Connection getConnection() {
        Connection conn = connection.get();
        if (conn == null) {
            conn = DataSource.getConnection();
            connection.set(conn);
        }
        return conn;
    }

    public static void beginTransaction() throws SQLException {
        getConnection().setAutoCommit(false);
    }

    public static void commit() throws SQLException {
        getConnection().commit();
    }

    public static void rollback() throws SQLException {
        getConnection().rollback();
    }

    public static void close() {
        connection.remove();
    }
}

Spring 中的 ThreadLocal

Spring 大量使用 ThreadLocal 来传递上下文:

  • RequestContextHolder:存储当前 HTTP 请求
  • TransactionSynchronizationManager:管理事务上下文
  • SecurityContextHolder:存储安全认证信息

理解 ThreadLocal,是理解 Spring 框架内部机制的基础。

注意事项

ThreadLocal 不是万能钥匙,有些坑需要避开:

  1. 必须清理:使用完毕后调用 remove(),否则可能导致内存泄漏
  2. 子线程不继承:普通 ThreadLocal,子线程无法访问父线程的值(需要用 InheritableThreadLocal)
  3. 线程池问题:线程池复用的线程,如果上次的值没清理,会被下个任务看到
  4. 不是线程同步:ThreadLocal 不能替代 synchronized,它解决的是隔离问题,不是同步问题

要点回顾

  • ThreadLocal 为每个线程提供独立的变量副本
  • 值存储在 Thread.threadLocals 中,不是存储在 ThreadLocal 本身
  • 使用 get() / set() / remove() 操作
  • 使用 withInitial() 或重写 initialValue() 设置初始值
  • 用完必须 remove(),特别是线程池场景

相关链接

基于 VitePress 构建