简体   繁体   中英

Is it dangerous to use ThreadLocal with ExecutorService?

I was going through the concept of ThreadLocals on the below blog :

https://www.baeldung.com/java-threadlocal

It says that "Do not use ThreadLocal with ExecutorService"

It illustrates below example for using ThreadLocals.

public class ThreadLocalWithUserContext implements Runnable {
  
    private static ThreadLocal<Context> userContext 
      = new ThreadLocal<>();
    private Integer userId;
    private UserRepository userRepository = new UserRepository();
 
    @Override
    public void run() {
        String userName = userRepository.getUserNameForUserId(userId);
        userContext.set(new Context(userName));
        System.out.println("thread context for given userId: "
          + userId + " is: " + userContext.get());
    }
     
    // standard constructor
}

At the End of the post it mentions that :

If we want to use an ExecutorService and submit a Runnable to it, using ThreadLocal will yield non-deterministic results – because we do not have a guarantee that every Runnable action for a given userId will be handled by the same thread every time it is executed.

Because of that, our ThreadLocal will be shared among different userIds. That's why we should not use a TheadLocal together with ExecutorService. It should only be used when we have full control over which thread will pick which runnable action to execute.

This explanation was a bouncer to me. I tried to do some research online for this point specifically but I could not get much help, can some expert please elaborate on the above explanation? Is it authors view or a real Threat?

Consider a ThreadLocal to be some sort of "in memory cache" for code that is executed by the same thread. The exact same thread. It is a bad idea to share a ThreadLocal between code that is executed on different threads.

Tha javadoc clearly states:

This class provides thread-local variables. These variables differ from their normal counterparts in that each thread that accesses one (via its get or set method) has its own, independently initialized copy of the variable. ThreadLocal instances are typically private static fields in classes that wish to associate state with a thread (eg, a user ID or Transaction ID).

In other words: the goal of using ThreadLocals is to give "each" code running in different threads "thread specific" data.

ExecutorService on the other hand is first of all an interface: you simply don't know if it is powered by a single thread, or (much more likely) by multiple threads.

In other words: using an ExecutorService quickly leads to multiple different threads running your Runnables/Tasks. And then you would be sharing your ThreadLocal amongst these multiple threads.

So, "dangerous" is maybe the wrong word. The goal of using ThreadLocal is to have per-thread storage, whereas an ExecutorService is about code being executed by an unknown number of threads. Those two things are simply not going together nicely.

The focus is different: one concept emphasises a long living thread connected to a very specific "activity". The other concept is about executing small, independent activities using an unknown number of namesless threads.

The point of that caution is that multiple runs of your Runnable may execute on different threads. An executor service can be backed by a single thread but it may just as well be backed by a pool of threads. On subsequent executions of your Runnable , a different thread will be accessing a different ThreadLocal .

So you certainly can use ThreadLocal within a single run of the Runnable . But it is not likely to be useful, as generally the purpose of a ThreadLocal is to hold a value for a while. In contrast, a Runnable should generally be short-lived.

So, no, generally it does not make sense to use a ThreadLocal with a thread pool.

ThreadLocal will yield non-deterministic results – because we do not have a guarantee that every Runnable action for a given userId will be handled by the same thread every time it is executed.

In the code example posted, the argument above is invalid because the ThreadLocal value is set when run() is called therefore any subsequent get() within the same block is deterministic regardless of using a ExecutorService .

Calling set(new Context()) in Runnable A then get() from another Runnable B is not deterministic because you have no control over which Thread the Runnable is being executed.

Just assume that the object returned by get() could be anything unless you know when it was last set.

ThreadLocal is used when you want to cache a variable in that thread after you set it. So next time when you want to access it, you can just directly get it from the ThreadLocal without initialization.

Since you set it with threadLocal.set(obj) and you access it via threadLocal.get() within a thread, so directly you have thread-safe guarantee.

But things might become ugly, if you do not clear the cache by threadLocal.remove() explicitly.

  1. in a thread pool, the tasks queued up will be processed by threads one by one and the task should be independent most of the time, but the thread-scope cache threadLocal will make the following tasks depend on its previous if you forget to clear it first before handling the next task;

  2. the cached threadLocals will not be gc-ed immediately (at some unknown moment - out of your control) since their keys are WeakReference , which might cause OOM without your knowing.

A simple demo for such case where remove() is not explicitly invoked causing an OOM.

public class ThreadLocalOne {
    private static final int THREAD_POOL_SIZE = 500;
    private static final int LIST_SIZE = 1024 * 25;

    private static ThreadLocal<List<Integer>> threadLocal = new ThreadLocal<>();

    public static void main(String[] args) throws InterruptedException {

        ExecutorService executorService = Executors.newFixedThreadPool(THREAD_POOL_SIZE);

        for (int i = 0; i < THREAD_POOL_SIZE; i++) {
            executorService.execute(() -> {
                threadLocal.set(getBigList());
                System.out.println(Thread.currentThread().getName() + " : " + threadLocal.get().size());
                // threadLocal.remove(); 
                // explicitly remove the cache, OOM shall not occur;
            });
        }
        executorService.shutdown();
    }

    private static List<Integer> getBigList() {
        List<Integer> ret = new ArrayList<>();
        for (int i = 0; i < LIST_SIZE; i++) {
            ret.add(i);
        }
        return ret;
    }
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM