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

时间:2019-02-16 17:55:27

标签: java multithreading thread-local

我在下面的博客中介绍了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一起使用。仅当我们完全控制哪个线程将选择要执行的可运行操作时,才应使用它。”

这个解释对我来说是个保镖。我试图为此专门在网上进行一些研究,但无法获得太多帮助,请一些专家详细说明上述解释吗?是作者观点还是真正的威胁?

4 个答案:

答案 0 :(得分:4)

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

Tha javadoc明确指出:

  

此类提供线程局部变量。这些变量与普通变量不同,因为每个访问一个线程(通过其get或set方法)的线程都有其自己的,独立初始化的变量副本。 ThreadLocal实例通常是希望将状态与线程关联的类中的私有静态字段(例如,用户ID或交易ID)。

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

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

换句话说:使用ExecutorService会迅速导致多个不同的线程运行您的Runnables / Tasks。然后,您将在多个线程之间共享您的ThreadLocal。

因此,“危险”一词可能是错误的。使用ThreadLocal的目标是按线程存储的,而ExecutorService是关于由未知个线程执行的代码。这两件事根本不能很好地融合在一起。

关注点有所不同:一个概念强调了与非常具体的“活动”相关的长期存在的线程。另一个概念是关于使用未知数量的无名线程执行小的独立活动。

答案 1 :(得分:3)

that caution的意义在于,Runnable多个运行可能在不同的线程上执行。可执行程序服务可以由单个线程支持,但也可以由线程池支持。在Runnable的后续执行中,另一个线程将访问另一个ThreadLocal

因此,您肯定 可以在Runnable的一次运行中使用ThreadLocal。但这不太可能有用,因为通常ThreadLocal的目的是保留一个值一段时间。相反,Runnable通常应该是短命的。

因此,不,通常在线程池中使用ThreadLocal是没有意义的。

答案 2 :(得分:1)

  

ThreadLocal将产生不确定的结果–因为我们不能保证每次给定userId的每个Runnable动作都将由同一线程处理。

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

先在set(new Context())中调用Runnable A然后再从另一个get()调用Runnable B是不确定的,因为您无法控制Thread是哪个Runnable被执行。

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

答案 3 :(得分:0)

ThreadLocal用于在设置变量后 在该线程中缓存变量的情况。因此,下次您要访问它时,可以直接从ThreadLocal获取它,而无需初始化。

由于您使用threadLocal.set(obj)进行了设置,并且您在线程内通过threadLocal.get()对其进行了访问,因此直接具有线程安全保证。

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

  1. 在线程池中,排队的任务将由线程一个接一个地处理,并且该任务在大多数情况下应该是独立的,但是线程范围缓存threadLocal将使以下任务取决于其先前的任务如果您忘记在处理下一个任务之前先清除它;

  2. 已缓存线程本地变量不会被立即进行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;
    }
}