ThreadLocal将数据绑定到特定线程。对于CompletableFuture,它使用来自线程池的线程执行,该线程池可能是不同的线程。
这是否意味着在执行CompletableFuture时,它可能无法从ThreadLocal获取数据?
答案 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会有两个后果:
所以,需要非常小心,避免在 CompletableFuture 中使用 ThreadLocal。