web-dev-qa-db-fra.com

Comment nettoyer ThreadLocals

Quelqu'un at-il un exemple comment faire cela? Sont-ils gérés par le ramasse-miettes? J'utilise Tomcat 6.

52
Ricardo

Le javadoc dit ceci:

"Chaque thread contient une référence implicite à sa copie d'une variable de thread local tant que le thread est actif et que l'instance ThreadLocal est accessible; après qu'un thread se ferme, toutes ses copies d'instances de threads locales sont soumises au nettoyage de la mémoire. (sauf si d'autres références à ces copies existent).

Si votre application ou (si vous parlez de threads de requête) utilise un pool de threads, cela signifie que les threads ne meurent pas. Si nécessaire, vous devrez vous occuper vous-même des fils de discussion locaux. La seule façon propre de procéder consiste à appeler la méthode ThreadLocal.remove().

Il existe deux raisons pour lesquelles vous pourriez vouloir nettoyer les sections locales de thread pour les threads d'un pool de threads:

  • pour empêcher les fuites de mémoire (ou de ressources hypothétiques), ou 
  • pour éviter les fuites accidentelles d'informations d'une requête à une autre via des sections locales de threads.

Les fuites de mémoire locales de threads ne devraient normalement pas être un problème majeur avec les pools de threads liés, car tout fichier local de thread sera probablement écrasé; c'est-à-dire lorsque le fil est réutilisé. Cependant, si vous commettez l'erreur de créer maintes et maintes fois une nouvelle instance ThreadLocal (au lieu d'utiliser une variable static pour contenir une instance singleton), les valeurs locales du thread ne seront pas écrasées et s'accumuleront dans la mappe threadlocals de chaque thread. Cela pourrait entraîner une fuite grave.


En supposant que vous parlez de sections locales de threads créées/utilisées lors du traitement d'une requête HTTP par une application Web, un moyen d'éviter les fuites locales de l'unité de thread consiste à enregistrer une ServletRequestListener avec la ServletContext de votre application Web et à implémenter la méthode requestDestroyed de l'écouteur pour la nettoyer. les sections locales pour le thread actuel.

Notez que dans ce contexte, vous devez également envisager la possibilité de informations de fuir d’une requête à une autre.

62
Stephen C

Voici du code pour nettoyer toutes les variables locales de thread du thread actuel lorsque vous ne disposez pas d'une référence à la variable locale de thread actuelle. Vous pouvez également le généraliser pour nettoyer les variables locales du thread pour d'autres threads:

    private void cleanThreadLocals() {
        try {
            // Get a reference to the thread locals table of the current thread
            Thread thread = Thread.currentThread();
            Field threadLocalsField = Thread.class.getDeclaredField("threadLocals");
            threadLocalsField.setAccessible(true);
            Object threadLocalTable = threadLocalsField.get(thread);

            // Get a reference to the array holding the thread local variables inside the
            // ThreadLocalMap of the current thread
            Class threadLocalMapClass = Class.forName("Java.lang.ThreadLocal$ThreadLocalMap");
            Field tableField = threadLocalMapClass.getDeclaredField("table");
            tableField.setAccessible(true);
            Object table = tableField.get(threadLocalTable);

            // The key to the ThreadLocalMap is a WeakReference object. The referent field of this object
            // is a reference to the actual ThreadLocal variable
            Field referentField = Reference.class.getDeclaredField("referent");
            referentField.setAccessible(true);

            for (int i=0; i < Array.getLength(table); i++) {
                // Each entry in the table array of ThreadLocalMap is an Entry object
                // representing the thread local reference and its value
                Object entry = Array.get(table, i);
                if (entry != null) {
                    // Get a reference to the thread local object and remove it from the table
                    ThreadLocal threadLocal = (ThreadLocal)referentField.get(entry);
                    threadLocal.remove();
                }
            }
        } catch(Exception e) {
            // We will tolerate an exception here and just log it
            throw new IllegalStateException(e);
        }
    }
33
lyaffe

Il n'y a aucun moyen de nettoyer les valeurs ThreadLocal sauf à l'intérieur de le fil qui les a placées là en premier lieu (ou lorsque le fil de discussion est nettoyé, pas le cas avec les threads de travail). Cela signifie que vous devez prendre soin de nettoyer vos ThreadLocal lorsqu'une demande de servlet est terminée (ou avant de transférer AsyncContext vers un autre thread du Servlet 3), car après cela, vous ne pourrez plus jamais entrer dans ce thread de travail spécifique. perdra de la mémoire dans les situations où votre application Web n'est pas déployée alors que le serveur n'est pas redémarré.

Un bon endroit pour faire un tel nettoyage est ServletRequestListener.requestDestroyed () .

Si vous utilisez Spring, tout le câblage nécessaire est déjà en place, vous pouvez simplement insérer des éléments dans l'étendue de votre demande sans vous soucier de les nettoyer (cela se produit automatiquement):

RequestContextHolder.getRequestAttributes().setAttribute("myAttr", myAttr, RequestAttributes.SCOPE_REQUEST);
. . .
RequestContextHolder.getRequestAttributes().getAttribute("myAttr", RequestAttributes.SCOPE_REQUEST);
15
rustyx

Relire attentivement la documentation Javadoc:

'Chaque thread contient une référence implicite à sa copie d'une variable locale au thread tant que le thread est actif et que l'instance ThreadLocal est accessible. après la disparition d'un thread, toutes ses copies d'instances locales à un thread sont soumises au nettoyage de la mémoire (à moins qu'il n'existe d'autres références à ces copies) . '.

Il n'est pas nécessaire de nettoyer quoi que ce soit, il existe une condition "ET" pour que la fuite puisse survivre. Ainsi, même dans un conteneur Web où le fil survit à l’application, Tant que la classe webapp est déchargée (seule la référence dans une classe statique chargée dans le chargeur de classe parent empêcherait cela et cela n’a problèmes généraux liés aux fichiers partagés contenant des données statiques), la deuxième étape de la condition AND n’est plus remplie et la copie locale du thread est donc éligible pour le nettoyage de la mémoire.

Thread local ne peut pas être la cause de fuites de mémoire, dans la mesure où l'implémentation respecte la documentation.

1
Philippe P.

La machine virtuelle Java nettoie automatiquement tous les objets sans référence contenus dans l'objet ThreadLocal.

Une autre façon de nettoyer ces objets (par exemple, ces objets peuvent être tous les objets non sécurisés qui existent autour d'un fil) consiste à les placer dans une classe Object Holder, qui le contient et vous pouvez remplacer la méthode finalize pour nettoyer l'objet qui résident en elle. Encore une fois, cela dépend du collecteur de place et de ses politiques, lorsqu'il invoquerait la méthode finalize.

Voici un exemple de code:

public class MyObjectHolder {

    private MyObject myObject;

    public MyObjectHolder(MyObject myObj) {
        myObject = myObj;
    }

    public MyObject getMyObject() {
        return myObject;
    }

    protected void finalize() throws Throwable {
        myObject.cleanItUp();
    }
}

public class SomeOtherClass {
    static ThreadLocal<MyObjectHolder> threadLocal = new ThreadLocal<MyObjectHolder>();
    .
    .
    .
}
0
Parasu

J'aimerais apporter ma réponse à cette question même si elle est ancienne. J'avais le même problème (gson threadlocal n'est pas supprimé du thread de requête) et je me suis même habitué à redémarrer le serveur à chaque fois qu'il manquait de mémoire (ce qui est une grosse perte de temps !!). 

Dans le contexte d'une application Web Java configurée en mode dev (en ce sens que le serveur est configuré pour rebondir à chaque fois qu'il détecte une modification du code et qu'il s'exécute éventuellement en mode débogage), j'ai rapidement compris que le langage de thread peut être génial. et parfois être une douleur. J'utilisais une invocation threadlocal pour chaque demande. À l'intérieur de l'invocation. J'utilisais parfois aussi gson pour générer ma réponse. Je placerais l'Invocation dans un bloc "try" dans le filtre et le détruirais dans un bloc "finally".

Ce que j'ai observé (je n'ai pas de métrique pour sauvegarder cela pour l'instant), c'est que si j'apportais des modifications à plusieurs fichiers et que le serveur rebondissait constamment entre mes modifications, je devenais impatient et redémarrais le serveur (Tomcat pour être précis). de l'IDE. Probablement qu'autrement, je finirais avec une exception «Mémoire insuffisante».

J'ai réussi à inclure une implémentation ServletRequestListener dans mon application et mon problème a disparu. Je pense que ce qui se passait, c’est qu’au milieu d’une requête, si le serveur devait rebondir plusieurs fois, mes threadlocals n’étaient pas clarifiés (gson inclus), je recevais donc cet avertissement concernant les threadlocals et deux ou trois avertissements plus tard, le serveur planterait. Avec ServletResponseListener fermant explicitement mes threadlocals, le problème de gson a disparu.

J'espère que cela a du sens et vous donne une idée de la façon de surmonter les problèmes locaux. Fermez-les toujours autour de leur point d'utilisation. Dans ServletRequestListener, testez chaque encapsuleur de thread et s'il existe toujours une référence valide à un objet, détruisez-le à ce stade.

Je devrais également signaler que prendre l'habitude d'envelopper un threadlocal en tant que variable statique dans une classe. Ainsi, vous pouvez être assuré qu'en le détruisant dans ServeltRequestListener, vous n'aurez plus à vous soucier des autres instances de la même classe.

0
mainas

La réponse de @ lyaffe est la meilleure possible pour Java 6. Cette solution résout quelques problèmes en utilisant ce qui est disponible dans Java 8.

La réponse de @ lyaffe a été écrite pour Java 6 avant que MethodHandle ne soit disponible. Il souffre de pénalités de performance dues à la réflexion. Si utilisé comme ci-dessous, MethodHandle fournit zéro surcharge accès aux champs et aux méthodes.

La réponse de @ lyaffe passe aussi explicitement par le ThreadLocalMap.table et est sujette aux bogues. Il existe une méthode ThreadLocalMap.expungeStaleEntries() maintenant disponible qui fait la même chose.

Le code ci-dessous contient 3 méthodes d'initialisation pour minimiser le coût d'invocation de expungeStaleEntries().

private static final MethodHandle        s_getThreadLocals     = initThreadLocals();
private static final MethodHandle        s_expungeStaleEntries = initExpungeStaleEntries();
private static final ThreadLocal<Object> s_threadLocals        = ThreadLocal.withInitial(() -> getThreadLocals());

public static void expungeThreadLocalMap()
{
   Object threadLocals;

   threadLocals = s_threadLocals.get();

   try
   {
      s_expungeStaleEntries.invoke(threadLocals);
   }
   catch (Throwable e)
   {
      throw new IllegalStateException(e);
   }
}

private static Object getThreadLocals()
{
   ThreadLocal<Object> local;
   Object result;
   Thread thread;

   local = new ThreadLocal<>();

   local.set(local);   // Force ThreadLocal to initialize Thread.threadLocals

   thread = Thread.currentThread();

   try
   {
      result = s_getThreadLocals.invoke(thread);
   }
   catch (Throwable e)
   {
      throw new IllegalStateException(e);
   }

   return(result);
}

private static MethodHandle initThreadLocals()
{
   MethodHandle result;
   Field field;

   try
   {
      field = Thread.class.getDeclaredField("threadLocals");

      field.setAccessible(true);

      result = MethodHandles.
         lookup().
         unreflectGetter(field);

      result = Preconditions.verifyNotNull(result, "result is null");
   }
   catch (NoSuchFieldException | SecurityException | IllegalAccessException e)
   {
      throw new ExceptionInInitializerError(e);
   }

   return(result);
}

private static MethodHandle initExpungeStaleEntries()
{
   MethodHandle result;
   Class<?> clazz;
   Method method;
   Object threadLocals;

   threadLocals = getThreadLocals();
   clazz        = threadLocals.getClass();

   try
   {
      method = clazz.getDeclaredMethod("expungeStaleEntries");

      method.setAccessible(true);

      result = MethodHandles.
         lookup().
         unreflect(method);
   }
   catch (NoSuchMethodException | SecurityException | IllegalAccessException e)
   {
      throw new ExceptionInInitializerError(e);
   }

   return(result);
}
0
Nathan