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 和特定线程关联的数据。