Search code examples
javathread-safetythread-local

ThreadLocal value not cleared for the next thread in java


I'm running a web service which is receiving 200 RPS at least. Based on the action, we provide root access for few operations and using the following code.

private static final ThreadLocal<String> rootContext = new ThreadLocal<String>();

public Optional<String> getRunner() {
    if (rootContext.get() != null) {
        return rootContext.get();
    } else {
        return getCurrentRunner();
    }
}

public void rootAccess(Runnable runnable) {
    rootContext.set("root");
    runnable.run();
    rootContext.set(null);
}

getCurrentRunner() method will return the actual caller based on the request. The problem is 1 request out of 200 requests returns root instead of the actual caller.

One thing I noticed is instead of using threadlocal.remove(), I'm setting that value as null. Expecting that, getRunner() rootContext.get() != null condition will fail and return the actual caller.

How to solve this ? Will setting rootContext.remove() solve this ? If yes, how ?

Thanks for the help


Solution

  • There are two problems with your rootAccess method:

    1. if the Runnable throws a RuntimeException the ThreadLocal is not removed (probably what you are seeing)
    2. rootContext.set(null); still keeps the ThreadLocal instance associated with the running thread, it is better to do rootContext.remove();

    Correcting this two points means to change rootAccess() to

    public void rootAccess(Runnable runnable) {
        rootContext.set("root");
        try {
            runnable.run();
        } finally {
            rootContext.remove();
        }
    }
    

    Why is rootContext.set(null); generally a problem?

    Each thread basically keeps a data structure similar to a Map<ThreadLocal, ?> where the key is your ThreadLocal instance (rootContext) and the value is the value that you associate with it through rootContext.set(xx);

    If you call rootContext.set(null); then rootContext is still in that map and therefore each thread (from a thread pool, meaning the thread is long running) that executed this line keeps a reference to rootContext which in turn might prevent class unloading.

    If you call rootContext.remove(); the rootContext is removed from that map.