将ThreadLocal与ExecutorService一起使用是危险的吗?

问题描述 投票:8回答:4

我在下面的博客上看到了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一起使用。只有当我们完全控制哪个线程将选择要执行哪个可运行动作时,才应该使用它。“

这个解释对我来说是一个保镖。我特意在网上做了一些研究,但是我得不到多少帮助,有些专家请详细说明上述解释吗?它是作者观点还是真正的威胁?

java multithreading thread-local
4个回答
3
投票

that caution的观点是你的Runnable的多次运行可以在不同的线程上执行。执行程序服务可以由单个线程支持,但它也可以由线程池支持。在你的Runnable的后续执行中,一个不同的线程将访问不同的ThreadLocal

所以你肯定可以在ThreadLocal的一次运行中使用Runnable。但它不太可能有用,因为一般来说,ThreadLocal的目的是保持一段时间的价值。相比之下,Runnable通常应该是短暂的。

所以,不,通常使用带有线程池的ThreadLocal是没有意义的。


4
投票

将ThreadLocal视为由同一线程执行的代码的某种“内存缓存”。完全相同的线程。在不同线程上执行的代码之间共享ThreadLocal是个坏主意。

Tha javadoc明确指出:

该类提供线程局部变量。这些变量与它们的正常对应物的不同之处在于,访问一个变量的每个线程(通过其get或set方法)都有自己独立初始化的变量副本。 ThreadLocal实例通常是希望将状态与线程(例如,用户ID或事务ID)相关联的类中的私有静态字段。

换句话说:使用ThreadLocals的目的是让“每个”代码在不同的线程中运行“特定于线程”的数据。

另一方面,ExecutorService首先是一个接口:你根本不知道它是由单个线程驱动,还是(更有可能)由多个线程驱动。

换句话说:使用ExecutorService可以快速导致运行Runnables / Tasks的多个不同线程。然后你将在这些多线程中共享你的ThreadLocal。

所以,“危险”可能是错误的词。使用ThreadLocal的目的是使用每线程存储,而ExecutorService则是由未知数量的线程执行的代码。这两件事根本不能很好地融合在一起。

重点不同:一个概念强调与一个非常具体的“活动”相关的长生活线索。另一个概念是使用未知数量的无名线程执行小型独立活动。


1
投票

ThreadLocal将产生非确定性结果 - 因为我们无法保证每次执行时,同一个线程的每个Runnable操作都将由同一个线程处理。

在发布的代码示例中,上面的参数无效,因为在调用ThreadLocal时设置了run()值,因此无论使用get(),同一块内的任何后续ExecutorService都是确定性的。

set(new Context())中调用Runnable A然后从另一个get()调用Runnable B不是确定性的,因为你无法控制Thread执行的Runnable

假设get()返回的对象可能是任何东西,除非你知道它最后一次设置的时间。


0
投票

如果要在设置后在该线程中缓存变量,则使用ThreadLocal。因此,下次当您想要访问它时,您可以直接从ThreadLocal获取它而无需初始化。

由于您使用threadLocal.set(obj)进行设置,并且您通过线程中的threadLocal.get()访问它,因此您可以直接使用线程安全保证。

但是如果你没有明确地通过threadLocal.remove()清除缓存,事情可能会变得很丑陋。

  1. 在线程池中,排队的任务将由线程逐个处理,并且任务在大多数时间应该是独立的,但是线程范围缓存threadLocal将使以下任务依赖于之前的任务,如果您忘记清除它在处理下一个任务之前;
  2. 缓存的threadLocals不会立即被gc-ed(在某个未知的时刻 - 超出你的控制范围),因为它们的密钥是WeakReference,这可能会在你不知情的情况下导致OOM。

这种情况的简单演示,其中没有显式调用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;
    }
}
© www.soinside.com 2019 - 2024. All rights reserved.