【问题标题】:Is it dangerous to use ThreadLocal with ExecutorService?将 ThreadLocal 与 ExecutorService 一起使用是否危险?
【发布时间】:2019-07-10 13:58:15
【问题描述】:

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

这个解释对我来说是一个保镖。我试图专门针对这一点在网上做一些研究,但我没有得到太多帮助,请高手详细说明一下上述解释吗?是作者的观点还是真正的威胁?

【问题讨论】:

  • 只要在每个任务开始的时候初始化ThreadLocal就可以了。
  • 为什么?如果您使用的是Executor Service,则您已经拥有RunnablesCallables,您可以将数据保存在其中。不需要ThreadLocal

标签: java multithreading thread-local


【解决方案1】:

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

所以您当然可以Runnable 的单次运行中使用ThreadLocal。但它不太可能有用,因为通常ThreadLocal 的目的是保持一个值一段时间。相反,Runnable 通常应该是短暂的。

所以,不,通常将ThreadLocal 与线程池一起使用是没有意义的。

【讨论】:

  • ThreadLocalWithUserContext firstUser = new ThreadLocalWithUserContext(1);让我们暂时忘记线程池并深入了解根目录,假设我上面的可运行“firstUser”由两个不同的线程执行,每个线程都将拥有其 ThreadLocal 的副本。所以,我同意这会增加内存占用空间并使代码容易受到内存泄漏的影响,但我没有看到它对程序的正确性或最终输出的影响,我的理解是否正确?如果不是,那您能举例说明一下吗?
  • 可能他们只是想复用这个变量,减少GC的频率。
【解决方案2】:

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

javadoc 明确指出:

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

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

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

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

所以,“危险”可能是错误的词。使用 ThreadLocal 的目标是每个线程存储,而 ExecutorService 是关于由 未知 个线程执行的代码。这两件事简直不能很好地结合在一起。

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

【讨论】:

  • 抱歉,我没听懂?我们怎么能说“ThreadLocal”将在池中的线​​程之间共享,因为池中的每个线程都能够保存自己的 ThreadLocal 个人副本?
  • 你能通过一些例子解释一下这样做会影响程序的正确性吗?
  • 转身:你为什么不去看看创建一个在通过执行器服务使用的同时以有意义的方式使用本地线程的示例?你认为这是一个很好的方法,所以你为什么不看看它在现实中是如何工作的?!从那里开始。不要让其他人为你学习。如果您一开始就没有“看这里很有意义”的例子,那么要求反例是没有意义的!
  • 请注意:我并不是说这种方法是危险的或总是导致错误的代码。我只是说这两个概念实现了在某种程度上相互矛盾的不同目标!
  • 不明白
【解决方案3】:

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

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

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

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

【讨论】:

    【解决方案4】:

    ThreadLocal 用于在设置变量之后 缓存该线程中的变量。所以下次你想访问它时,你可以直接从 ThreadLocal 中获取它,而不需要初始化。

    由于您使用threadLocal.set(obj) 设置它并在线程内通过threadLocal.get() 访问它,因此您直接获得了线程安全保证。

    但如果你不明确地清除threadLocal.remove()缓存,事情可能会变得很糟糕。

    1. 在线程池中,排队的任务会被线程一个一个处理,大部分时间任务应该是独立的,但是线程范围缓存threadLocal会使后面的任务依赖于它之前的任务如果您忘记在处理下一个任务之前先清除它;

    2. cached threadLocals 不会立即被 gc-ed(在某些 unknown 时刻 - 超出您的控制),因为它们的键是 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;
        }
    }
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2010-12-28
      • 2010-10-05
      • 2019-11-28
      • 1970-01-01
      • 2023-04-07
      • 2016-04-19
      • 2010-11-30
      • 2017-08-21
      相关资源
      最近更新 更多