简体   繁体   English

将 ThreadLocal 与 CompletableFuture 一起使用是否安全?

[英]Is it safe to use ThreadLocal with CompletableFuture?

ThreadLocal binds data to a particular thread. ThreadLocal 将数据绑定到特定线程。 For CompletableFuture, it executes with a Thread from thread pool, which might be different thread.对于 CompletableFuture,它使用来自线程池的线程执行,该线程可能是不同的线程。

Does that mean when CompletableFuture is executed, it may not be able to get the data from ThreadLocal?那是不是说当CompletableFuture被执行时,它可能无法从ThreadLocal中获取数据呢?

each thread that accesses ThreadLocal (via its get or set method) has its own, independently initialized copy of the variable每个访问 ThreadLocal 的线程(通过其 get 或 set 方法)都有自己的、独立初始化的变量副本

so different threads will receive different values when using ThreadLocal.get ;所以不同的线程在使用ThreadLocal.get时会收到不同的值; also different threads will set their own value when using ThreadLocal.set ;使用ThreadLocal.set时,不同的线程也会设置自己的值; there'll be no overlapping of the ThreadLocal's inner/stored/own value between different threads.不同线程之间的 ThreadLocal 的内部/存储/自己的值不会重叠。

But because the question is about the safety in combination with thread pool I'll point a specific risk specific to that special combination:但是因为问题是关于与线程池结合的安全性,我将指出特定于该特殊组合的特定风险:

for a sufficient number of calls exists the chance that the threads in the pool are reused (that's the whole point of the pool :)).对于足够数量的调用,池中的线程有可能被重用(这就是池的全部意义:))。 Let's say we have pool-thread1 which executed task1 and now is executing task2;假设我们有 pool-thread1,它执行了 task1,现在正在执行 task2; task2 will reuse the same ThreadLocal value as task1 if task1 didn't remove it from the ThreadLocal before finishing its job!如果 task1 在完成其工作之前没有将其从ThreadLocal删除,则 task2 将重用与 task1 相同的 ThreadLocal 值! and reuse might not be what you want.并且重用可能不是您想要的。

Check the tests below;检查下面的测试; they might better prove my point.他们可能会更好地证明我的观点。

package ro.go.adrhc.concurrent;

import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;

import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadLocalRandom;

import static org.junit.Assert.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotEquals;

@Slf4j
class ThreadLocalTest {
    /**
     * Have 1 thread in order to have the 100% chance of 2 tasks using same copy of the ThreadLocal variable.
     */
    private ExecutorService es = Executors.newSingleThreadExecutor();
    private ThreadLocal<Double> cache = new ThreadLocal<>();
    /**
     * Random initialization isn't an alternative for proper cleaning!
     */
    private ThreadLocal<Double> cacheWithInitVal = ThreadLocal.withInitial(
            ThreadLocalRandom.current()::nextDouble);

    @Test
    void reuseThreadWithCleanup() throws ExecutionException, InterruptedException {
        var future1 = es.submit(() -> this.doSomethingWithCleanup(cache));
        var future2 = es.submit(() -> this.doSomethingWithCleanup(cache));
        assertNotEquals(future1.get(), future2.get()); // different runnable just used a different ThreadLocal value
    }

    @Test
    void reuseThreadWithoutInitVal() throws ExecutionException, InterruptedException {
        var future1 = es.submit(() -> this.doSomething(cache));
        var future2 = es.submit(() -> this.doSomething(cache));
        assertEquals(future1.get(), future2.get()); // different runnable just used the same ThreadLocal value
    }

    @Test
    void reuseThreadWithInitVal() throws ExecutionException, InterruptedException {
        var future1 = es.submit(() -> this.doSomething(cacheWithInitVal));
        var future2 = es.submit(() -> this.doSomething(cacheWithInitVal));
        assertEquals(future1.get(), future2.get()); // different runnable just used the same ThreadLocal value
    }

    private Double doSomething(ThreadLocal<Double> cache) {
        if (cache.get() == null) {
            // reusing ThreadLocal's value when not null
            cache.set(ThreadLocalRandom.current().nextDouble());
        }
        log.debug("thread: {}, cache: {}", Thread.currentThread().toString(), cache.get());
        return cache.get();
    }

    private Double doSomethingWithCleanup(ThreadLocal<Double> cache) {
        try {
            return doSomething(cache);
        } finally {
            cache.remove();
        }
    }
}

yes, scala Futures have Local.scala that transfers state through the thenCompose/thenApply methods (ie. flatMap/map methods).是的,scala Futures 有 Local.scala,它通过 thenCompose/thenApply 方法(即 flatMap/map 方法)传输状态。 Java is really missing that making it VERY hard on platform developers creating a platform to pass state from platform to client code which then calls back into the platform. Java 确实缺少使平台开发人员很难创建平台以将状态从平台传递到客户端代码,然后客户端代码回调到平台。 It is quite frustrating but scala has way too many features(it's like the kitchen sink) so while concise, I find projects grow out of control faster as humans(including me) all tend to make different choices.这很令人沮丧,但 scala 有太多功能(就像厨房水槽)所以虽然简洁,但我发现项目发展得更快,因为人类(包括我)都倾向于做出不同的选择。

Generally speaking, the answer is NO, it's not safe to use ThreadLocal in CompletableFuture.一般来说,答案是否定的,在 CompletableFuture 中使用 ThreadLocal 是不安全的。 The main reason is that the ThreadLocal variables are thread-bounded.主要原因是 ThreadLocal 变量是线程有界的。 These variables are targeted to be used in the CURRENT thread.这些变量的目标是在 CURRENT 线程中使用。 However, the backend of CompletableFuture is Thread Pool, which means the threads are shared by multiples tasks in random order.但是 CompletableFuture 的后端是线程池,这意味着线程被多个任务以随机顺序共享。 There will be two consequences to use ThreadLocal:使用 ThreadLocal 会有两个后果:

  1. Your task can not get ThreadLocal variables since the thread in the thread pool does not know the original thread's ThreadLocal variables您的任务无法获取 ThreadLocal 变量,因为线程池中的线程不知道原始线程的 ThreadLocal 变量
  2. If you force to put the ThreadLocal variables in your task which executing by CompleatableFuture, it will 'pollute' other task's ThreadLocal variables.如果您强制将 ThreadLocal 变量放在由 CompleatableFuture 执行的任务中,它将“污染”其他任务的 ThreadLocal 变量。 Eg if you store user information in ThreadLocal, one user may accidentally get another user's information.例如,如果您将用户信息存储在 ThreadLocal 中,一个用户可能会意外获得另一个用户的信息。

So, it needs to be very careful to avoid using ThreadLocal in CompletableFuture.所以,需要非常小心,避免在 CompletableFuture 中使用 ThreadLocal。

当我尝试在 CompletableFuture 中使用 ThreadLocal 时,终端会抛出“java.util.concurrent.ExecutionException: java.lang.NullPointerException”所以答案是否定的

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM