web-dev-qa-db-fra.com

Propagation de ThreadLocal vers un nouveau thread récupéré depuis un ExecutorService

J'exécute un processus dans un thread séparé avec un délai d'attente, en utilisant un ExecutorService et un Future (exemple de code ici ) (le thread "en train de se créer" se déroule dans un aspect AOP).

Maintenant, le fil principal est un Resteasy request. Resteasy utilise une ou plusieurs variables ThreadLocal pour stocker certaines informations de contexte que je dois extraire à un moment donné de mon appel à la méthode Rest. Le problème, c'est que le thread Resteasy s'exécutant dans un nouveau thread, les variables ThreadLocal sont perdues.

Quel serait le meilleur moyen de "propager" la variable ThreadLocal utilisée par Resteasy dans le nouveau thread? Il semble que Resteasy utilise plus d'une variable ThreadLocal pour garder une trace des informations de contexte et je voudrais transférer "à l'aveuglette" toutes les informations dans le nouveau thread.

J'ai examiné les sous-classes ThreadPoolExecutor et l'utilisation de la méthode beforeExecute pour transmettre le thread actuel au pool, mais je n'ai pas trouvé de moyen de transmettre les variables ThreadLocal au pool.

Toute suggestion?

Merci

25
Luciano Fiandesio

L'ensemble d'instances ThreadLocal associées à un thread sont conservées dans des membres privés de chaque Thread. Votre seule chance de les énumérer est de faire une réflexion sur la Thread; De cette façon, vous pouvez remplacer les restrictions d'accès sur les champs du thread.

Une fois que vous pouvez obtenir l'ensemble ThreadLocal, vous pouvez copier dans les threads d'arrière-plan à l'aide des crochets beforeExecute() et afterExecute() de ThreadPoolExecutor ou en créant un wrapper Runnable pour intercepter l'appel run() afin de définir les instances ThreadLocal nécessaires. En réalité, cette dernière technique fonctionnera peut-être mieux, dans la mesure où elle vous donnerait un emplacement pratique pour stocker les valeurs ThreadLocal au moment où la tâche est mise en file d'attente.


Mise à jour: Voici une illustration plus concrète de la deuxième approche. Contrairement à ma description initiale, tout ce qui est stocké dans le wrapper est le thread appelant, qui est interrogé lors de l'exécution de la tâche.

static Runnable wrap(Runnable task)
{
  Thread caller = Thread.currentThread();
  return () -> {
    Iterable<ThreadLocal<?>> vars = copy(caller);
    try {
      task.run();
    }
    finally {
      for (ThreadLocal<?> var : vars)
        var.remove();
    }
  };
}

/**
 * For each {@code ThreadLocal} in the specified thread, copy the thread's 
 * value to the current thread.  
 * 
 * @param caller the calling thread
 * @return all of the {@code ThreadLocal} instances that are set on current thread
 */
private static Collection<ThreadLocal<?>> copy(Thread caller)
{
  /* Use a nasty bunch of reflection to do this. */
  throw new UnsupportedOperationException();
}
19
erickson

Si je comprends votre problème, vous pouvez consulter InheritableThreadLocal qui est destiné à transmettre les variables ThreadLocal du contexte de thread parent au contexte de thread enfant.

3
Grooveek

Basé sur la réponse de @erickson, j'ai écrit ce code. Il travaille pour inheritableThreadLocals. Il construit la liste de héritableThreadLocals en utilisant la même méthode que celle utilisée dans le contrôleur Thread. Bien sûr, j'utilise la réflexion pour le faire. Aussi, je remplace la classe de l'exécuteur.

public class MyThreadPoolExecutor extends ThreadPoolExecutor
{
   @Override
   public void execute(Runnable command)
   {
      super.execute(new Wrapped(command, Thread.currentThread()));
   }
}

Wrapper:

   private class Wrapped implements Runnable
   {
      private final Runnable task;

      private final Thread caller;

      public Wrapped(Runnable task, Thread caller)
      {
         this.task = task;
         this.caller = caller;
      }

      public void run()
      {
         Iterable<ThreadLocal<?>> vars = null;
         try
         {
            vars = copy(caller);
         }
         catch (Exception e)
         {
            throw new RuntimeException("error when coping Threads", e);
         }
         try {
            task.run();
         }
         finally {
            for (ThreadLocal<?> var : vars)
               var.remove();
         }
      }
   }

méthode de copie:

public static Iterable<ThreadLocal<?>> copy(Thread caller) throws Exception
   {
      List<ThreadLocal<?>> threadLocals = new ArrayList<>();
      Field field = Thread.class.getDeclaredField("inheritableThreadLocals");
      field.setAccessible(true);
      Object map = field.get(caller);
      Field table = Class.forName("Java.lang.ThreadLocal$ThreadLocalMap").getDeclaredField("table");
      table.setAccessible(true);

      Method method = ThreadLocal.class
              .getDeclaredMethod("createInheritedMap", Class.forName("Java.lang.ThreadLocal$ThreadLocalMap"));
      method.setAccessible(true);
      Object o = method.invoke(null, map);

      Field field2 = Thread.class.getDeclaredField("inheritableThreadLocals");
      field2.setAccessible(true);
      field2.set(Thread.currentThread(), o);

      Object tbl = table.get(o);
      int length = Array.getLength(tbl);
      for (int i = 0; i < length; i++)
      {
         Object entry = Array.get(tbl, i);
         Object value = null;
         if (entry != null)
         {
            Method referentField = Class.forName("Java.lang.ThreadLocal$ThreadLocalMap$Entry").getMethod(
                    "get");
            referentField.setAccessible(true);
            value = referentField.invoke(entry);
            threadLocals.add((ThreadLocal<?>) value);
         }
      }
      return threadLocals;
   }
2
Mr Jedi

Voici un exemple pour passer le LocaleContext actuel dans le thread parent au thread enfant couvert par CompletableFuture [Par défaut, il utilisait ForkJoinPool]. 

Définissez simplement tout ce que vous voulez faire dans un thread enfant dans un bloc Runnable. Ainsi, lorsque CompletableFuture exécute le bloc Runnable, c'est le thread enfant qui contrôle et le tour est joué, le contenu de ThreadLocal du parent est défini dans ThreadLocal de Child. 

Le problème ici n’est pas que l’ensemble de ThreadLocal soit copié. Seul le LocaleContext est copié. Étant donné que ThreadLocal n’est réservé qu’à un fil, il appartient également à Reflection. Essayer d’obtenir un réglage dans Child est tout simplement une chose loufoque qui pourrait entraîner des fuites de mémoire ou une baisse des performances. 

Donc, si vous connaissez les paramètres du ThreadLocal qui vous intéressent, cette solution fonctionne mieux. 

 public void parentClassMethod(Request request) {
        LocaleContext currentLocale = LocaleContextHolder.getLocaleContext();
        executeInChildThread(() -> {
                LocaleContextHolder.setLocaleContext(currentLocale);
                //Do whatever else you wanna do
            }));

        //Continue stuff you want to do with parent thread
}


private void executeInChildThread(Runnable runnable) {
    try {
        CompletableFuture.runAsync(runnable)
            .get();
    } catch (Exception e) {
        LOGGER.error("something is wrong");
    }
}
0
Seetharamani Tmr

Je n'aime pas l'approche de réflexion. Une solution alternative consisterait à implémenter l'exécuteur wrapper et à transmettre l'objet directement sous la forme d'un contexte ThreadLocal à tous les fils enfants propageant un contexte parent. 

public class PropagatedObject {

    private ThreadLocal<ConcurrentHashMap<AbsorbedObjectType, Object>> data = new ThreadLocal<>();

   //put, set, merge methods, etc

}

==>

public class ObjectAwareExecutor extends AbstractExecutorService {

    private final ExecutorService delegate;
    private final PropagatedObject objectAbsorber;

    public ObjectAwareExecutor(ExecutorService delegate, PropagatedObject objectAbsorber){
        this.delegate = delegate;
        this.objectAbsorber = objectAbsorber;
    }
    @Override
    public void execute(final Runnable command) {

        final ConcurrentHashMap<String, Object> parentContext = objectAbsorber.get();
        delegate.execute(() -> {
            try{
                objectAbsorber.set(parentContext);
                command.run();
            }finally {
                parentContext.putAll(objectAbsorber.get());
                objectAbsorber.clean();
            }
        });
        objectAbsorber.merge(parentContext);
    }
0
Wild Goat