1. 概述

在本教程中,我们将研究java.lang包中的ThreadLocal构造。这使我们能够单独存储当前线程的数据,并简单地将其包装在特殊类型的对象中。

2.ThreadLocal 接口

TheadLocal构造允许我们存储只能由特定线程访问的数据。

假设我们想要一个将与特定线程捆绑在一起的Integer值:

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

接下来,当我们想从线程中使用此值时,我们只需要调用get() 或set() 方法。简单地说,我们可以想象ThreadLocal以线程为键将数据存储在映射中。

因此,当我们在threadLocalValue 上调用get() 方法时,我们将得到请求线程的整数值:

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

我们可以通过使用withInitial()static方法并向其传递一个供应商来构造ThreadLocal的实例:

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

要从ThreadLocal 中删除该值,我们可以调用remove() 方法:

threadLocal.remove();

要了解如何正确使用ThreadLocal,我们将首先查看一个不使用ThreadLocal 的示例,然后我们将重写我们的示例以利用该构造。

3. 在Map中存储用户数据

让我们考虑一个程序,该程序需要为每个给定用户 ID 存储特定于用户的上下文数据:

public class Context {
    private String userName;

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

我们希望每个用户 ID 有一个线程。我们将创建一个实现Runnable接口的SharedMapWithUserContext类。run() 方法中的实现通过UserRepository类调用某个数据库,该类返回给定userId 的上下文对象。

接下来,我们将该上下文存储在由userId键控的ConcurentHashMap中:

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));
    }

    // standard constructor
}

我们可以通过为两个不同的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实例。

使用 ThreadLocal 时,我们需要非常小心,因为每个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());
    }
    
    // standard constructor
}

我们可以通过启动两个线程来测试它,这两个线程将为给定的 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'}

我们可以看到每个用户都有自己的上下文

5.ThreadLocal 和线程

ThreadLocal提供了一个易于使用的 API,用于将一些值限制为每个线程。这是在 Java 中实现线程安全的合理方法。但是,当我们一起使用ThreadLocal和线程池时,我们应该格外小心。

为了更好地理解这个可能的警告,让我们考虑以下场景:

  1. 首先,应用程序从池中借用线程。
  2. 然后,它将一些线程限制的值存储到当前线程的ThreadLocal 中。
  3. 当前执行完成后,应用程序会将借用的线程返回到池中。
  4. 一段时间后,应用程序借用同一线程来处理另一个请求。
  5. 由于应用程序上次未执行必要的清理,因此它可能会对新请求重复使用相同的ThreadLocal数据。

这可能会在高并发应用程序中造成令人惊讶的后果。

解决此问题的一种方法是在使用完每个ThreadLocal后手动删除它。由于此方法需要严格的代码审查,因此容易出错。

5.1. 扩展线程池执行器

事实证明,可以扩展ThreadPoolExecutor类并为 beforeExecute() 和afterExecute( 方法提供自定义钩子实现。线程池将在使用借用的线程运行任何内容之前调用beforeExecute() 方法。另一方面,它将在执行我们的逻辑后调用afterExecute() 方法。

因此,我们可以扩展ThreadPoolExecutor类并删除afterExecute() 方法中的ThreadLocal数据:

public class ThreadLocalAwareThreadPool extends ThreadPoolExecutor {

    @Override
    protected void afterExecute(Runnable r, Throwable t) {
        // Call remove on each ThreadLocal
    }
}

如果我们向ExecutorService 的实现提交请求,那么我们可以确定使用ThreadLocal和线程池不会给我们的应用程序带来安全隐患。

6. 结论

在这篇简短的文章中,我们研究了ThreadLocal构造。我们实现了使用在线程之间共享的ConcurrentHashMap来存储与特定userId关联的上下文的逻辑。然后,我们重写了示例,以利用ThreadLocal来存储与特定userId和特定线程关联的数据。