Java 将ThreadLocal与ExecutorService一起使用是否危险?
我在下面的博客上浏览了ThreadLocals的概念: 它说“不要将ThreadLocal与ExecutorService一起使用” 下面是使用ThreadLocals的示例Java 将ThreadLocal与ExecutorService一起使用是否危险?,java,multithreading,thread-local,Java,Multithreading,Thread Local,我在下面的博客上浏览了ThreadLocals的概念: 它说“不要将ThreadLocal与ExecutorService一起使用” 下面是使用ThreadLocals的示例 public class ThreadLocalWithUserContext implements Runnable { private static ThreadLocal<Context> userContext = new 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
}
公共类ThreadLocalWithUserContext实现可运行{
私有静态线程本地用户上下文
=新的ThreadLocal();
私有整数用户标识;
private UserRepository UserRepository=new UserRepository();
@凌驾
公开募捐{
字符串userName=userRepository.getUserNameForUserId(userId);
set(新上下文(用户名));
System.out.println(“给定用户ID的线程上下文:”
+userId+是:“+userContext.get());
}
//标准构造器
}
文章结尾提到:
如果我们想使用ExecutorService并向其提交一个Runnable,那么使用ThreadLocal将产生不确定的结果——因为我们不能保证给定用户ID的每个Runnable操作在每次执行时都由同一个线程处理
因此,我们的ThreadLocal将在不同的用户ID之间共享。这就是为什么我们不应该将本地服务与Executor服务一起使用的原因。只有当我们完全控制哪个线程将选择执行哪个可运行操作时,才应该使用它
这个解释对我来说是一个错误。我试着在网上做一些专门针对这一点的研究,但我没有得到太多的帮助,能否请一些专家详细说明上述解释?这是作者的观点还是真正的威胁?问题的关键是,您的Runnable
的多次运行可能会在不同的线程上执行。执行器服务可以由单个线程支持,但也可以由线程池支持。在后续执行Runnable
时,不同的线程将访问不同的ThreadLocal
所以您当然可以在Runnable
的单个运行中使用。但它不太可能有用,因为通常ThreadLocal
的目的是保存一个值一段时间。相反,Runnable
通常应该是短期的
因此,不,通常将
ThreadLocal
与线程池一起使用是没有意义的 将ThreadLocal视为由同一线程执行的代码的某种“内存缓存”。完全一样的线。在不同线程上执行的代码之间共享ThreadLocal不是一个好主意
该文件明确指出:
此类提供线程局部变量。这些变量不同于它们的普通对应变量,因为访问一个变量的每个线程(通过其get或set方法)都有自己的、独立初始化的变量副本。ThreadLocal实例通常是希望将状态与线程(例如,用户ID或事务ID)关联的类中的私有静态字段
换句话说:使用ThreadLocals的目标是为在不同线程中运行的“每个”代码提供“特定于线程”的数据
另一方面,ExecutorService首先是一个接口:您根本不知道它是由单个线程驱动的,还是(更可能)由多个线程驱动的
换句话说:使用ExecutorService会很快导致多个不同的线程运行您的可运行程序/任务。然后,您将在这些多个线程之间共享您的ThreadLocal
所以,“危险”可能是个错误的词。使用ThreadLocal的目标是拥有每个线程的存储空间,而ExecutorService是由未知数量的线程执行的代码。这两件事根本没有很好地结合在一起
重点是不同的:一个概念强调与一个非常具体的“活动”相关的长期存在的线索。另一个概念是使用未知数量的无名称线程执行小型独立活动
ThreadLocal将产生不确定的结果——因为我们不能保证给定用户ID的每个可运行操作在每次执行时都由同一线程处理
在发布的代码示例中,上述参数无效,因为ThreadLocal
值是在调用run()
时设置的,因此同一块中的任何后续get()
都是确定的,无论使用的是ExecutorService
在Runnable A
中调用set(new Context())
,然后从另一个Runnable B
调用get()
是不确定的,因为您无法控制执行Runnable
的线程
只要假设
get()
返回的对象可以是任何对象,除非您知道上次设置该对象的时间。ThreadLocal用于在设置后要在该线程中缓存变量。因此,下次当您想要访问它时,您可以直接从ThreadLocal获取它,而无需初始化
由于您使用threadLocal.set(obj)
设置它,并通过线程内的threadLocal.get()
访问它,因此直接就可以保证线程安全
但是如果不明确地通过threadLocal.remove()
清除缓存,事情可能会变得糟糕
remove()
的情况
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;
}
}