你了解Java中的ThreadLocal吗?
ThreadLocal
是一个在多线程编程中使用的概念,通常在 Java 语言中使用。ThreadLocal
类提供了线程局部变量的存储,这些变量对于每个线程都是唯一的,即每个线程可以访问自己的 ThreadLocal
变量副本,而不会与其他线程冲突。
1. 概述
在本教程中,我们将研究java.lang包中的 *ThreadLocal*构造。这使我们能够单独存储当前线程的数据并将其简单地包装在特殊类型的对象中。
ThreadLocal
类的主要用途包括:
- 避免线程间的数据冲突:当多个线程需要共享某些数据但又不希望它们相互影响时,使用
ThreadLocal
可以为每个线程提供一个独立的变量副本。 - 减少同步开销:由于每个线程访问的是自己的数据副本,因此不需要进行同步,这可以减少因同步带来的性能开销。
- 保持线程安全:使用
ThreadLocal
可以自然地实现线程安全,因为每个线程只能访问自己的数据副本。 - 实现线程局部状态:在某些情况下,线程可能需要维护自己的局部状态或配置信息,
ThreadLocal
提供了一种方便的方式来实现这一点。
2. ThreadLocal API
TheadLocal构造允许我们存储只能由特定线程访问的数据。
假设我们想要一个与特定线程捆绑在一起的整数值:
ThreadLocal<Integer> threadLocalValue = new ThreadLocal<>();
接下来,当我们想从线程中使用这个值时,我们只需要调用get() 或set() 方法。简单地说,我们可以想象ThreadLocal将数据存储在一个以线程为键的映射中。
因此,当我们在threadLocalValue上调用get() 方法时,我们将获得请求线程的整数值:
threadLocalValue.set(1);
Integer result = threadLocalValue.get();
我们可以使用withInitial() 静态方法并将供应商传递给它来构造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的 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映射中有两个条目,来轻松测试我们的代码:
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 中实现[线程安全的合理方法。但是,当我们将]ThreadLocal 和 线程池 一起 **使用时,我们应该格外小心 。
为了更好地理解这个可能的警告,让我们考虑以下场景:
- 首先,应用程序从线程池中借用一个线程。
- 然后它将一些线程限制的值存储到当前线程的 ThreadLocal中。
- 一旦当前执行完成,应用程序就会将借用的线程返回到池中。
- 过了一会儿,应用程序借用同一个线程来处理另一个请求。
- 由于应用程序上次没有执行必要的清理,它可能会 针对新请求重新使用相同的ThreadLocal数据。
这可能会在高度并发的应用程序中造成令人惊讶的后果。
解决这个问题的一个方法是, 在使用完每个ThreadLocal后手动将其删除。由于这种方法需要严格的代码审查,因此很容易出错。
扩展 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来存储与特定 userId 关联的上下文的逻辑 。 然后我们重写了示例,利用ThreadLocal来存储与特定 userId和特定线程关联的数据。
转载自:https://juejin.cn/post/7387934821186175014