写点什么

Java 中的秘会厅 ThreadLocal 你了解吗?

  • 2023-12-30
    河北
  • 本文字数:2628 字

    阅读完需:约 9 分钟

1.ThreadLocal 概览

在 Java 的编程魔法世界的 java.lang 包中有个 ThreadLocal 结构,这使我们能够为当前线程单独存储数据,并将其简单地封装在一个特殊类型的对象中。

2.ThreadLocal API

TheadLocal 结构允许我们存储只能由特定线程访问的数据。 比如,我们希望将一个整数值与特定线程捆绑在一起:


ThreadLocal<Integer> threadLocalValue = new ThreadLocal<>();
复制代码


接下来,当我们要使用线程中的这个值时,只需调用 get() 或 set() 方法即可。简单地说,我们可以想象 ThreadLocal 将数据存储在一个以线程为键的映射中。因此,当我们调用 threadLocalValue 上的 get() 方法时,我们将得到请求线程的整数值:


threadLocalValue.set(1);Integer result = threadLocalValue.get();
复制代码


要从 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 类调用数据库,该数据库会返回给定用户 ID 的 Context 对象。


接下来,我们将该上下文存储在以 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}
复制代码


我们可以通过启动两个线程来测试它,这两个线程将执行给定用户 ID 的操作:


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 后,手动将其删除。由于这种方法需要严格的代码审查,因此容易出错。

6.ThreadPoolExecutor 扩展

事实证明,扩展 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 和线程池不会给我们的应用程序带来安全隐患。

在本文中,我们研究了 ThreadLocal 结构。我们实现了使用线程间共享的 ConcurrentHashMap 来存储与特定用户 ID 关联的上下文的逻辑。然后,我们重写了示例,利用 ThreadLocal 来存储与特定用户 ID 和特定线程相关的数据。


发布于: 刚刚阅读数: 4
用户头像

还未添加个人签名 2018-04-26 加入

还未添加个人简介

评论

发布
暂无评论
Java中的秘会厅ThreadLocal你了解吗?_Java_骨灰架构师虫哥_InfoQ写作社区