侧边栏壁纸
博主头像
翻斗

开始一件事最好是昨天,其次是现在

  • 累计撰写 44 篇文章
  • 累计创建 42 个标签
  • 累计收到 3 条评论

Java中的ThreadLocal

翻斗
2020-03-04 / 0 评论 / 0 点赞 / 656 阅读 / 4,174 字

Baeldung 翻译系列之Java并发基础:

Java中的concurrent包
Java中的Synchronized关键字
Future介绍
ThreadLocal介绍
Java线程的生命周期
如何杀掉一个Java线程
Java中的线程池介绍
实现Runnable接口还是继承Thread类
Java中的wait和notify方法
Runnable vs Callable
wait和sleep的区别
Thread.join方法介绍
Java中使用锁对象
ThreadPoolTaskExecutor中的corePoolSize和maxPoolSize
Java中的异步编程

1. 概览

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

2. ThreadLocal API

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

假设我们想要将一个整数值与特定线程绑定:

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

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

因此,当我们在threadLocalValue上调用get()方法时,我们将为请求的线程获取一个整数值:

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

我们可以使用withInitial()静态方法并传递一个供应商(supplier)来构建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类调用一些数据库,该类返回给定userIdContext对象。

接下来,我们将该上下文存储在以 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));
    }

    // standard constructor
}

我们可以通过为两个不同的用户ID创建并启动两个线程,并断言在 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 来存储用户 Context 实例。每个线程将有自己的 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'}

我们可以看到每个用户都有自己的 Context

5. ThreadLocal和线程池

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

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

1、首先,应用程序从池中借用一个线程。
2、然后,它将一些线程限制的值存储到当前线程的 ThreadLocal 中。
3、一旦当前执行结束,应用程序将借来的线程返回给池。
4、过一会儿,应用程序借用同一个线程来处理另一个请求。
5、由于应用程序上次没有进行必要的清理,它可能会在新的请求中重用相同的 ThreadLocal 数据。

这可能在高并发应用程序中导致出人意料的后果。

解决这个问题的一种方法是在我们使用完每个 · 之后手动删除它。因为这种方法需要严格的代码审查,所以可能容易出错。

5.1. 扩展 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 和线程池不会为我们的应用程序引入安全隐患。

6. 总结

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

所有这些例子和代码片段的实现可以在 GitHub 或者Gitee 上找到。

0

评论区