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

1

1

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();
Distraught answered 27/7, 2021 at 17:13 Comment(0)
E
2

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.

Eleanore answered 28/7, 2021 at 3:53 Comment(5)
1. "Child thread as separate object" => It's done during the child Threads creation for the ThreadPool associated with ExecutorService per the implementation of ThreadLocal (per my quick look into how childValue() method is used by createInheritedMap() method) so the child threads do not have exact reference as parent. 2. "Your childValue() implementation is exactly the same as the default." => Yes I know, it is just for sake of reference (updated the comments on code).Distraught
3. "Can only work with un-pooled threads" => Yes, hence I am looking for alternatives. Can we use something like @RequestScoped injection? But seems that internally uses ThreadLocal too, so would have similar concerns. 4. "Third-party code" => At least for my case I am not using third-party code. Do you mean something else here? 5. "Pass the ServiceDoc as parameter" => Yes that's my last resort. Will initialize ThreadLocal only once in class A and fetch ServiceDoc from ThreadLocal in class B and Y only. Y can use as it is, and B will pass the ServiceDoc to Task C. So, in total 2 accesses.Distraught
@Distraught The child threads definitely do have the same reference as the parent. That is, (if you don't override childValue()) the initial value returned to a child thread from a call to get() is identical to that returned to the parent thread. If you are talking about the map used internal to the thread local, yes, those are distinct, because if the child thread reassigns the thread local, other threads, including the parent, must not see that change. As a simplification, the child thread has its own "variable", but it's initially set to the same value as the parent thread.Eleanore
Yes I was referring about ThreadLocal's createInheritedMap() method. Sorry but your first and last line seems a bit contradicting (maybe its the language). I guess what you mean is that a shallow copy of the parent context is passed to the child context?Distraught
One more question for you just in case you are willing to answer: #68555518 :)Distraught

© 2022 - 2024 — McMap. All rights reserved.