Is it safe to use ThreadLocal with CompletableFuture?

ThreadLocal binds data to a particular thread. For CompletableFuture, it executes with a Thread from thread pool, which might be different thread.

Does that mean when CompletableFuture is executed, it may not be able to get the data from ThreadLocal?


Solution 1:

each thread that accesses ThreadLocal (via its get or set method) has its own, independently initialized copy of the variable

so different threads will receive different values when using ThreadLocal.get; also different threads will set their own value when using ThreadLocal.set; there'll be no overlapping of the ThreadLocal's inner/stored/own value between different threads.

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; task2 will reuse the same ThreadLocal value as task1 if task1 didn't remove it from the ThreadLocal before finishing its job! 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();
        }
    }
}

Solution 2:

yes, scala Futures have Local.scala that transfers state through the thenCompose/thenApply methods (ie. flatMap/map methods). 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. 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.

Solution 3:

Generally speaking, the answer is NO, it's not safe to use ThreadLocal in CompletableFuture. The main reason is that the ThreadLocal variables are thread-bounded. These variables are targeted to be used in the CURRENT thread. However, the backend of CompletableFuture is Thread Pool, which means the threads are shared by multiples tasks in random order. There will be two consequences to use 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
  2. If you force to put the ThreadLocal variables in your task which executing by CompleatableFuture, it will 'pollute' other task's ThreadLocal variables. E.g. if you store user information in ThreadLocal, one user may accidentally get another user's information.

So, it needs to be very careful to avoid using ThreadLocal in CompletableFuture.