Java 将ThreadLocal与ExecutorService一起使用是否危险?

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<>();

我在下面的博客上浏览了ThreadLocals的概念:

它说“不要将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
}
公共类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()
清除缓存,事情可能会变得糟糕

  • 在线程池中,排队的任务将由线程逐个处理,并且任务在大多数情况下应该是独立的,但是如果在处理下一个任务之前忘记先清除,则线程作用域缓存threadLocal将使以下任务依赖于其上一个任务

  • 缓存的threadLocals不会立即被gc删除(在某个未知的时刻-您无法控制),因为它们的键是,这可能会在您不知道的情况下导致OOM

  • 这是一个简单的演示,用于未显式使用
    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;
        }
    }