Java 中的 ThreadLocal 简介-Java快速进阶教程
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和线程池时,我们应该格外小心。
为了更好地理解这个可能的警告,让我们考虑以下场景:
- 首先,应用程序从池中借用线程。
- 然后,它将一些线程限制的值存储到当前线程的ThreadLocal 中。
- 当前执行完成后,应用程序会将借用的线程返回到池中。
- 一段时间后,应用程序借用同一线程来处理另一个请求。
- 由于应用程序上次未执行必要的清理,因此它可能会对新请求重复使用相同的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和特定线程关联的数据。