Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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?

like image 593
Franz Wong Avatar asked Dec 05 '25 15:12

Franz Wong


2 Answers

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();
        }
    }
}
like image 101
adrhc Avatar answered Dec 07 '25 04:12

adrhc


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.

like image 28
Kaneg Avatar answered Dec 07 '25 05:12

Kaneg



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!