Search code examples
javamultithreadingconcurrencythread-localinheritable-thread-local

How to share parent ThreadLocal object reference with the Child threads?


Use case

I have a gRPC+Guice based service application where for a particular call the code flow looks like: A -> B -> C and A -> X -> Y for a particular service request.

where, A = Top level Service operation/Activity class; B = Class which creates ExecutorService Threadpool with class C as task; X and Y are normal classes.

I want a shared object ContainerDoc across class B, C and Y these classes but do not want to pass on to method parameters. So, I have decided to use InheritableThreadLocal.

But I want to understand how to enforce sharing the parent ThreadLocal ContainerDoc to the Child Threads, so that any updates done in the ContainerDoc by child Thread is also visible to parent thread?

  1. Does overriding childValue method to return same contained object as of parent to make it work? (See below implementation).
  2. How to ensure Thread-Safety?

Sample Implementation

class ContainerDoc implements ServiceDoc {
    private final Map < KeyEnum, Object > containerMap;

    public ContainerDoc() {
        this.containerMap = new HashMap < KeyEnum, Object > ();
        // Should it be ConcurrentHashmap to account for concurrent updates?
    }

    public < T > T getEntity(final KeyEnum keyEnum) {
        return (T) containerMap.get(keyEnum);
    }

    public void putEntity(final KeyEnum keyEnum, final Object value) {
        entities.put(keyEnum, value);
    }

    enum KeyEnum {
        Key_A,
        Key_B;
    }

}

public enum MyThreadLocalInfo {

    THREADLOCAL_ABC(ContainerDoc.class, new InheritableThreadLocal < ServiceDoc > () {

            // Sets the initial value to an empty class instance.
            @Override
            protected ServiceContext initialValue() {
                return new ContainerDoc();
            }

            // Just for reference. The below impl shows default 
            // behavior. This method is invoked when any new
            // thread is created from a parent thread.
            // This ensures every child thread will have same 
            // reference as parent.
            @Override
            protected ServiceContext childValue(final ServiceDoc parentValue) {
                return parentValue;
                // Returning same reference but I think this 
                // value gets copied over to each Child thread as 
                // separate object instead of reusing the same 
                // copy for thread-safety. So, how to ensure 
                // using the same reference as of parent thread?
            }
        }),
        THREADLOCAL_XYZ(ABC.class, new InheritableThreadLocal < ServiceDoc > () {
            ....
            ....
        });

    private final Class << ? extends ServiceDoc > contextClazz;

    private final InheritableThreadLocal < ServiceDoc > threadLocal;

    MyThreadLocalInfo(final Class << ? extends ServiceDoc > contextClazz,
        final InheritableThreadLocal < ServiceDoc > threadLocal) {
        this.contextClazz = contextClazz;
        this.threadLocal = threadLocal;
    }

    public ServiceDoc getDoc() {
        return threadLocal.get();
    }

    public void setDoc(final ServiceDoc serviceDoc) {
        Validate.isTrue(contextClazz.isAssignableFrom(serviceDoc.getClass()));
        threadLocal.set(serviceDoc);
    }

    public void clearDoc() {
        threadLocal.remove();
    }
}

Client code (from Child Thread class or regular class

MyThreadLocalInfo.THREADLOCAL_ABC.setDoc(new ContainerDoc());
MyThreadLocalInfo.THREADLOCAL_ABC.getDoc().put(Key_A, new Object());
MyThreadLocalInfo.THREADLOCAL_ABC.clearDoc();

Solution

  • Returning same reference but I think this value gets copied over to each Child thread as separate object

    How would such a "separate object" be instantiated by the runtime? This theory is incorrect. Your childValue() implementation is exactly the same as the default.

    An InheritableThreadLocal is assigned a value based on the parent when a new thread is created. An ExecutorService could have any implementation, and you don't specify how yours creates threads, but for your approach to work, the parent thread would need to set the value, create a new thread, and then execute the task with that new thread. In other words, it can only work with un-pooled threads.

    ThreadLocal is a kludge to work around design flaws in third-party code that you can't change. Even if it works, it's a last resort—and here, it doesn't work.

    Pass the ServiceDoc as a method or constructor parameter as necessary to B, C, and Y.

    This probably means X needs to pass along the ServiceDoc as well, but, since there is no Executor involved in the X-Y code path, A could conditionally initialize a ThreadLocal before calling X. It's just probably uglier than passing it as a parameter.