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

时间:2017-03-18 04:30:55

标签: java asynchronous

ThreadLocal将数据绑定到特定线程。对于CompletableFuture,它使用来自线程池的线程执行,该线程池可能是不同的线程。

这是否意味着在执行CompletableFuture时,它可能无法从ThreadLocal获取数据?

3 个答案:

答案 0 :(得分:3)

(通过其get或set方法访问ThreadLocal的每个线程)具有 自己的,独立初始化的变量副本

因此,当使用ThreadLocal.get时,不同线程将收到不同的值;当使用ThreadLocal.set时,不同线程也会设置自己的值; 不同线程之间ThreadLocal的内部/存储的/自己的值不会重叠。

但是因为问题是关于安全性线程池的组合,所以我将指出针对该特殊组合的特定风险:

对于足够数量的调用,存在重用池中的线程的机会(这是池的全部要点:)。假设我们有执行任务1的pool-thread1,现在正在执行任务2;如果task1在完成工作之前没有从ThreadLocal中删除,则task2将重用与task1相同的ThreadLocal值!和 reuse 可能不是您想要的。

检查以下测试;他们可能会更好地证明我的观点。

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

答案 1 :(得分:0)

是的,scala Futures有Local.scala通过thenCompose / thenApply方法传递状态(即flatMap / map方法)。 Java真的很缺失,因为平台开发人员创建了一个平台,可以将状态从平台传递到客户端代码然后回调到平台。这非常令人沮丧,但scala有太多的功能(它就像厨房水槽),所以虽然简洁,我发现项目失控的速度更快,因为人类(包括我)都倾向于做出不同的选择。

答案 2 :(得分:0)

一般来说,答案是否定的,在 CompletableFuture 中使用 ThreadLocal 是不安全的。主要原因是 ThreadLocal 变量是线程有界的。这些变量的目标是在 CURRENT 线程中使用。但是 CompletableFuture 的后端是线程池,这意味着线程被多个任务以随机顺序共享。使用ThreadLocal会有两个后果:

  1. 您的任务无法获取 ThreadLocal 变量,因为线程池中的线程不知道原始线程的 ThreadLocal 变量
  2. 如果您强制将 ThreadLocal 变量放在由 CompleatableFuture 执行的任务中,它将“污染”其他任务的 ThreadLocal 变量。例如。如果您将用户信息存储在 ThreadLocal 中,一个用户可能会意外获得另一个用户的信息。

所以,需要非常小心,避免在 CompletableFuture 中使用 ThreadLocal。