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
类调用一些数据库,该类返回给定userId
的Context
对象。
接下来,我们将该上下文存储在以 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
和特定线程关联的数据。
评论区