3

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?

3
  • 1
    yes, exactly. Unless you prepare your own thread pool where each thread carries desired ThreadLocal variable. Commented Mar 18, 2017 at 8:13
  • @AlexeiKaigorodov How about if I use thenApply instead of thenApplyAsync, does that guarantee I can always execute in the same thread? So I can use ThreadLocal to store/read data.
    – Franz Wong
    Commented Mar 22, 2017 at 0:11
  • 1
    there is no guarantee on which thread thenApply is executed. If you want access to some common data, place that data in a class variable, and use a method of that class as a parameter to thenApply(). Commented Mar 22, 2017 at 8:00

4 Answers 4

3

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

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.

1

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.

-1

when i try to use ThreadLocal in CompletableFuture, the terminal will throw "java.util.concurrent.ExecutionException: java.lang.NullPointerException" so the answer is no

2

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.