Java 中的 ThreadLocal 简介

更新于 2025-12-29

baeldung 2025-02-15

1. 概述

在本教程中,我们将学习 java.lang 包中的 ThreadLocal 构造。它使我们能够为当前线程单独存储数据,并将其封装在一个特殊类型的对象中。

2. ThreadLocal API

ThreadLocal 构造允许我们存储仅由特定线程访问的数据。

假设我们希望有一个与特定线程绑定的 Integer 值:

ThreadLocal<Integer> threadLocalValue = new ThreadLocal<>();

当我们想从某个线程中使用该值时,只需调用 get()set() 方法即可。简单来说,我们可以将 ThreadLocal 想象成一个以线程为键(key)的内部 Map。

因此,当我们对 threadLocalValue 调用 get() 方法时,会获取到当前请求线程对应的 Integer 值:

threadLocalValue.set(1);
Integer result = threadLocalValue.get();

我们也可以通过 withInitial() 静态方法并传入一个 Supplier 来构造 ThreadLocal 实例:

ThreadLocal<Integer> threadLocal = ThreadLocal.withInitial(() -> 1);

要从 ThreadLocal 中移除值,可以调用 remove() 方法:

threadLocal.remove();

为了更好地理解如何正确使用 ThreadLocal,我们首先看一个不使用 ThreadLocal 的示例,然后将其重写为利用 ThreadLocal 的版本。

3. 在 Map 中存储用户数据

考虑一个需要为每个用户 ID 存储特定用户上下文(Context)数据的程序:

public class Context {
    private String userName;

    public Context(String userName) {
        this.userName = userName;
    }
}

我们希望每个用户 ID 对应一个线程。为此,我们创建一个实现了 Runnable 接口的 SharedMapWithUserContext 类。其 run() 方法通过 UserRepository 类调用数据库,根据给定的 userId 返回一个 Context 对象。

然后,我们将该上下文存储在一个以 userId 为键的 ConcurrentHashMap 中:

public class SharedMapWithUserContext implements Runnable {
 
    public static Map<Integer, Context> userContextPerUserId
      = new ConcurrentHashMap<>();
    private Integer userId;
    private UserRepository userRepository = new UserRepository();

    @Override
    public void run() {
        String userName = userRepository.getUserNameForUserId(userId);
        userContextPerUserId.put(userId, new Context(userName));
    }

    // 标准构造函数
}

我们可以通过创建并启动两个不同 userId 的线程来轻松测试这段代码,并断言 userContextPerUserId 映射中有两条记录:

SharedMapWithUserContext firstUser = new SharedMapWithUserContext(1);
SharedMapWithUserContext secondUser = new SharedMapWithUserContext(2);
new Thread(firstUser).start();
new Thread(secondUser).start();

assertEquals(SharedMapWithUserContext.userContextPerUserId.size(), 2);

4. 在 ThreadLocal 中存储用户数据

我们可以使用共享的 ThreadLocal 实例重写上述示例。每个线程都会在 ThreadLocal 对象中拥有自己的 Context

使用 ThreadLocal 时必须格外小心,因为存储在其中的每个对象都与特定线程相关联。在我们的示例中,每个 userId 都有我们自己创建的专用线程,因此我们对其拥有完全控制权。

run() 方法将获取用户上下文,并使用 set() 方法将其存入 ThreadLocal 变量中:

public class ThreadLocalWithUserContext implements Runnable {
 
    private static ThreadLocal<Context> userContext 
      = new ThreadLocal<>();
    private Integer userId;
    private UserRepository userRepository = new UserRepository();

    @Override
    public void run() {
        String userName = userRepository.getUserNameForUserId(userId);
        userContext.set(new Context(userName));
        System.out.println("thread context for given userId: " 
          + userId + " is: " + userContext.get());
    }
    
    // 标准构造函数
}

我们可以通过启动两个线程分别执行对应 userId 的操作来测试它:

ThreadLocalWithUserContext firstUser 
  = new ThreadLocalWithUserContext(1);
ThreadLocalWithUserContext secondUser 
  = new ThreadLocalWithUserContext(2);
new Thread(firstUser).start();
new Thread(secondUser).start();

运行此代码后,标准输出将显示每个线程都设置了各自的 ThreadLocal

thread context for given userId: 1 is: Context{userNameSecret='18a78f8e-24d2-4abf-91d6-79eaa198123f'}
thread context for given userId: 2 is: Context{userNameSecret='e19f6a0a-253e-423e-8b2b-bca1f471ae5c'}

可以看到,每个用户都有其独立的 Context

5. ThreadLocal 与线程池

ThreadLocal 提供了一个易于使用的 API,用于将某些值限定在每个线程内。这是在 Java 中实现线程安全的一种合理方式。然而,当我们将 ThreadLocal 与线程池一起使用时,必须格外小心。

为了更好地理解这一潜在问题,考虑以下场景:

  1. 应用程序从线程池中借出一个线程。
  2. 将某些线程专属的值存入当前线程的 ThreadLocal
  3. 当前任务执行完毕后,应用程序将线程归还给线程池。
  4. 一段时间后,应用程序再次借出同一个线程处理另一个请求。
  5. 由于上次未执行必要的清理操作,新请求可能会复用旧的 ThreadLocal 数据。

这在高并发应用中可能导致意外后果。

一种解决方案是在使用完 ThreadLocal 后手动调用 remove() 方法。但由于这种方法依赖严格的代码审查,容易出错。

5.1 扩展 ThreadPoolExecutor

实际上,我们可以扩展 ThreadPoolExecutor 类,并为 beforeExecute()afterExecute() 方法提供自定义钩子实现。线程池会在使用借出的线程执行任务前调用 beforeExecute() 方法,在执行完我们的逻辑后调用 afterExecute() 方法。

因此,我们可以在 afterExecute() 方法中清除 ThreadLocal 数据:

public class ThreadLocalAwareThreadPool extends ThreadPoolExecutor {

    @Override
    protected void afterExecute(Runnable r, Throwable t) {
        // 对每个 ThreadLocal 调用 remove()
    }
}

如果我们向这个 ExecutorService 的实现提交任务,就可以确保 ThreadLocal 与线程池结合使用时不会引入安全隐患。

6. 结论

在本文中,我们探讨了 ThreadLocal 构造。我们首先实现了一个使用 ConcurrentHashMap 在多个线程之间共享、并按特定 userId 存储上下文的逻辑;随后,我们将其重写为使用 ThreadLocal 来存储与特定 userId 和特定线程关联的数据。