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