Blogs

Blogs

Master Your ThreadLocals

ThreadLocal is not the "Silver Bullet" for concurrent issues, actually it is not encouraged to be used in some concurrent best practice.

But sometime, it is really needed or can significantly simplify your design. So we have to face it. Since it is very easy to be wrong used, we have to find a way to prevent it causing troubles. So today we are not talking about when and how to use ThreadLocal, but talking about when you are using it, how can you make sure it won't cause serious troubles.

The most serious error one could make with ThreadLocal is forgetting to reset it. If you are using ThreadLocal for caching user authentication, user A login your system through the service provided by worker thread 1, you cache the authentication info in a ThreadLocal for performance. But after worker thread 1 finishes the request for user A, you forget to reset the ThreadLocal(clear the cache). At this point, user B hits your system without login, but just happen to be served by worker thread 1, worker thread 1 simply check its cache for authentication, it will consider user B as user A. You can image what will happen next.

So the intuitive solution for this problem is reset the ThreadLocal after each request processing. But the difficult part is a work thread may have several ThreadLocal objects created from all around your application, how can you reset them all easily? You need a ThreadLocal registry of all ThreadLocal variables for each thread. Be awared! The registry itself also has to be a ThreadLocal object, so when a thread reset all ThreadLocal variables in the registry, it only resets its own ThreadLocal variables, not others. Once you have a registry like this, you can just reset the whole registry after each request processing, usually in a filter. Another question should come to your mind now is how can we register a ThreadLocal variable to the registry? Of course you can add a registering line after each ThreadLocal setting call. But this is really ungly and suffered from the same problem, what if you forget to add that registering call? The solution is creating a sub class of ThreadLocal, override the set() and initialValue() method, whenever this method is called, register itself to the registry. So by this way the whole registering and resetting process is transparent to programmer, all you need to do is use our sub class of ThreadLocal instead of the original ThreadLocal class.

Here are the sub class of ThreadLocal and the ThreadLocalRegistry:

public class AutoResetThreadLocal<T> extends InitialThreadLocal<T> {
    public AutoResetThreadLocal() {
        this(null);
    }
    public AutoResetThreadLocal(T initialValue) {
        super(initialValue);
    }
    public void set(T value) {
        ThreadLocalRegistry.registerThreadLocal(this);
        super.set(value);
    }
    protected T initialValue() {
        ThreadLocalRegistry.registerThreadLocal(this);
        return super.initialValue();
    }
}

public class ThreadLocalRegistry {
    public static ThreadLocal<?>[] captureSnapshot() {
        Set<ThreadLocal<?>> threadLocalSet = _threadLocalSet.get();
        return threadLocalSet.toArray(
            new ThreadLocal<?>[threadLocalSet.size()]);
    }
    public static void registerThreadLocal(ThreadLocal<?> threadLocal) {
        Set<ThreadLocal<?>> threadLocalSet = _threadLocalSet.get();
        threadLocalSet.add(threadLocal);
    }
    public static void resetThreadLocals() {
        Set<ThreadLocal<?>> threadLocalSet = _threadLocalSet.get();
        for (ThreadLocal<?> threadLocal : threadLocalSet) {
            threadLocal.remove();
        }
    }
    private static ThreadLocal<Set<ThreadLocal<?>>> _threadLocalSet =
        new InitialThreadLocal<Set<ThreadLocal<?>>>(
            new HashSet<ThreadLocal<?>>());
}

 Also a graph to demonstrate the registering and resetting:

Here are some advices:

  • Don't forget to reset your ThreadLocal, no matter how do you use it.
  • When your ThreadLocal's valid period is limited to request processing(or some other kinds of period time), try to use AutoResetThreadLocal and ThreadLocalRegistry to simplify your code.(The fewer lines you write, the fewer chance you make a mistake).
  • Be awared! You still need to call ThreadLocalRegistry.resetThreadLocals() somewhere(Usually in a filter).
3
Nice article. And this is just one of the possible problems. Forgetting to destroy the objects could lead to serious memory leaks too. And another problem which I discovered in Tomcat (and probably in other appservers as well), tomcat uses thread recycling. So if you forget to (re)set your threadlocal, you might see the old values from another request.
hey Shuyang, love your posts, keep them coming!!!
Nice article Shuyang.