我在下面的博客上看到了ThreadLocals的概念:
https://www.baeldung.com/java-threadlocal
它说“不要将ThreadLocal与ExecutorService一起使用”
它说明了下面使用ThreadLocals的示例。
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
}
在帖子的最后,它提到:
“如果我们想使用ExecutorService并向其提交Runnable,那么使用ThreadLocal将产生非确定性结果 - 因为我们无法保证每次给定userId的每个Runnable操作都将由同一个线程处理执行。
因此,我们的ThreadLocal将在不同的userId之间共享。这就是为什么我们不应该将TheadLocal与ExecutorService一起使用。只有当我们完全控制哪个线程将选择要执行哪个可运行动作时,才应该使用它。“
这个解释对我来说是一个保镖。我特意在网上做了一些研究,但是我得不到多少帮助,有些专家请详细说明上述解释吗?它是作者观点还是真正的威胁?
that caution的观点是你的Runnable
的多次运行可以在不同的线程上执行。执行程序服务可以由单个线程支持,但它也可以由线程池支持。在你的Runnable
的后续执行中,一个不同的线程将访问不同的ThreadLocal
。
所以你肯定可以在ThreadLocal
的一次运行中使用Runnable
。但它不太可能有用,因为一般来说,ThreadLocal
的目的是保持一段时间的价值。相比之下,Runnable
通常应该是短暂的。
所以,不,通常使用带有线程池的ThreadLocal
是没有意义的。
将ThreadLocal视为由同一线程执行的代码的某种“内存缓存”。完全相同的线程。在不同线程上执行的代码之间共享ThreadLocal是个坏主意。
Tha javadoc明确指出:
该类提供线程局部变量。这些变量与它们的正常对应物的不同之处在于,访问一个变量的每个线程(通过其get或set方法)都有自己独立初始化的变量副本。 ThreadLocal实例通常是希望将状态与线程(例如,用户ID或事务ID)相关联的类中的私有静态字段。
换句话说:使用ThreadLocals的目的是让“每个”代码在不同的线程中运行“特定于线程”的数据。
另一方面,ExecutorService首先是一个接口:你根本不知道它是由单个线程驱动,还是(更有可能)由多个线程驱动。
换句话说:使用ExecutorService可以快速导致运行Runnables / Tasks的多个不同线程。然后你将在这些多线程中共享你的ThreadLocal。
所以,“危险”可能是错误的词。使用ThreadLocal的目的是使用每线程存储,而ExecutorService则是由未知数量的线程执行的代码。这两件事根本不能很好地融合在一起。
重点不同:一个概念强调与一个非常具体的“活动”相关的长生活线索。另一个概念是使用未知数量的无名线程执行小型独立活动。
ThreadLocal将产生非确定性结果 - 因为我们无法保证每次执行时,同一个线程的每个Runnable操作都将由同一个线程处理。
在发布的代码示例中,上面的参数无效,因为在调用ThreadLocal
时设置了run()
值,因此无论使用get()
,同一块内的任何后续ExecutorService
都是确定性的。
在set(new Context())
中调用Runnable A
然后从另一个get()
调用Runnable B
不是确定性的,因为你无法控制Thread
执行的Runnable
。
假设get()
返回的对象可能是任何东西,除非你知道它最后一次设置的时间。
如果要在设置后在该线程中缓存变量,则使用ThreadLocal。因此,下次当您想要访问它时,您可以直接从ThreadLocal获取它而无需初始化。
由于您使用threadLocal.set(obj)
进行设置,并且您通过线程中的threadLocal.get()
访问它,因此您可以直接使用线程安全保证。
但是如果你没有明确地通过threadLocal.remove()
清除缓存,事情可能会变得很丑陋。
这种情况的简单演示,其中没有显式调用remove()
导致OOM。
public class ThreadLocalOne {
private static final int THREAD_POOL_SIZE = 500;
private static final int LIST_SIZE = 1024 * 25;
private static ThreadLocal<List<Integer>> threadLocal = new ThreadLocal<>();
public static void main(String[] args) throws InterruptedException {
ExecutorService executorService = Executors.newFixedThreadPool(THREAD_POOL_SIZE);
for (int i = 0; i < THREAD_POOL_SIZE; i++) {
executorService.execute(() -> {
threadLocal.set(getBigList());
System.out.println(Thread.currentThread().getName() + " : " + threadLocal.get().size());
// threadLocal.remove();
// explicitly remove the cache, OOM shall not occur;
});
}
executorService.shutdown();
}
private static List<Integer> getBigList() {
List<Integer> ret = new ArrayList<>();
for (int i = 0; i < LIST_SIZE; i++) {
ret.add(i);
}
return ret;
}
}