web-dev-qa-db-fra.com

Est-ce vraiment mon travail de nettoyer les ressources ThreadLocal lorsque les classes ont été exposées à un pool de threads?

Mon utilisation de ThreadLocal

Dans mes classes Java Java, j'utilise parfois un ThreadLocal principalement pour éviter la création inutile d'objets:

@net.jcip.annotations.ThreadSafe
public class DateSensitiveThing {

    private final Date then;

    public DateSensitiveThing(Date then) {
        this.then = then;
    }

    private static final ThreadLocal<Calendar> threadCal = new ThreadLocal<Calendar>()   {
        @Override
        protected Calendar initialValue() {
            return new GregorianCalendar();
        }
    };

    public Date doCalc(int n) {
        Calendar c = threadCal.get();
        c.setTime(this.then):
        // use n to mutate c
        return c.getTime();
    }
}

Je le fais pour la bonne raison - GregorianCalendar est l'un de ces objets glorieusement à état, modifiables et non threadsafe, qui fournit un service sur plusieurs appels, plutôt que de représenter une valeur. De plus, il est considéré comme "coûteux" d'instancier (que ce soit vrai ou non n'est pas le point de cette question). (Dans l'ensemble, je l'admire vraiment :-))

Comment Tomcat Whinges

Cependant, si j'utilise une telle classe dans n'importe quel environnement qui regroupe des threads - et où mon application ne contrôle pas le cycle de vie de ces threads - alors il y a un potentiel de fuites de mémoire. Un environnement Servlet en est un bon exemple.

En fait, Tomcat 7 pivote comme ça quand une webapp est arrêtée:

GRAVE: l'application Web [] a créé un ThreadLocal avec une clé de type [org.Apache.xmlbeans.impl.store.CharUtil $ 1] (valeur [org.Apache.xmlbeans.impl.store.CharUtil$1@2aace7a7]) et une valeur de type [Java.lang.ref.SoftReference] (valeur [Java.lang.ref.SoftReference@3d9c9ad4]) mais n'a pas pu le supprimer lors de l'arrêt de l'application Web. Les threads vont être renouvelés au fil du temps pour essayer d'éviter une fuite de mémoire probable. 13 décembre 2012 12:54:30 PM org.Apache.catalina.loader.WebappClassLoader checkThreadLocalMapForLeaks

(Pas même mon code qui le fait, dans ce cas particulier).

Qui est à blâmer?

Cela ne semble guère juste. Tomcat blâme moi (ou l'utilisateur de ma classe) pour avoir fait la bonne chose.

En fin de compte, c'est parce que Tomcat veut réutiliser les threads qu'il m'a proposés, pour autres applications Web. (Ugh - je me sens sale.) Probablement, ce n'est pas une excellente politique de la part de Tomcat - parce que les threads ont réellement/cause l'état - ne les partagez pas entre les applications.

Cependant, cette politique est au moins courante, même si elle n'est pas souhaitable. Je sens que je suis obligé - en tant qu'utilisateur ThreadLocal, de fournir à ma classe un moyen de "libérer" les ressources que ma classe a attachées à divers threads.

Mais que faire à ce sujet?

Quelle est la bonne chose à faire ici?

Pour moi, il semble que la politique de réutilisation des threads du moteur de servlet soit en contradiction avec l'intention derrière ThreadLocal.

Mais peut-être devrais-je fournir une fonctionnalité pour permettre aux utilisateurs de dire "fini, mauvais état spécifique au thread associé à cette classe, même si je ne suis pas en mesure de laisser le thread mourir et de laisser GC faire son travail?". Est-il même possible pour moi de le faire? Je veux dire, ce n'est pas comme si je pouvais arranger que ThreadLocal#remove() soit appelé sur chacun des Threads qui ont vu ThreadLocal#initialValue() à un moment donné dans le passé. Ou existe-t-il un autre moyen?

Ou devrais-je simplement dire à mes utilisateurs "allez vous procurer un chargeur de classe décent et une implémentation de pool de threads"?

EDIT # 1: Clarification de la façon dont threadCal est utilisé dans une classe utilitaire vanailla qui ignore les cycles de vie des threads EDIT # 2: Correction d'un problème de sécurité des threads dans DateSensitiveThing

44
David Bullock

Soupir, ce sont de vieilles nouvelles

Eh bien, un peu tard pour la fête sur celui-ci. En octobre 2007, Josh Bloch (co-auteur de Java.lang.ThreadLocal Avec Doug Lea) écrit :

"L'utilisation de pools de threads nécessite une extrême prudence. L'utilisation bâclée des pools de threads en combinaison avec une utilisation bâclée des sections locales de threads peut entraîner une rétention d'objet involontaire, comme cela a été noté à de nombreux endroits."

Les gens se plaignaient même de la mauvaise interaction de ThreadLocal avec les pools de threads. Mais Josh a sanctionné:

"Instances par thread pour les performances. L'exemple d'Aaron SimpleDateFormat (ci-dessus) est un exemple de ce modèle."

Quelques leçons

  1. Si vous placez tout type d'objets dans un pool d'objets, vous devez fournir un moyen de les supprimer "plus tard".
  2. Si vous "regroupez" en utilisant un ThreadLocal, vous avez des options limitées pour le faire. Soit: a) vous savez que les Thread (s) où vous mettez les valeurs se termineront lorsque votre application sera terminée; OR b) vous pouvez par la suite organiser le même thread qui a appelé ThreadLocal # set () pour appeler ThreadLocal # remove ( ) chaque fois que votre demande se termine
  3. En tant que tel, votre utilisation de ThreadLocal en tant que pool d'objets va coûter cher sur la conception de votre application et de votre classe. Les avantages ne sont pas gratuits.
  4. En tant que tel, l'utilisation de ThreadLocal est probablement une optimisation prématurée, même si Joshua Bloch vous a invité à le considérer dans "Java efficace".

En bref, décider d'utiliser un ThreadLocal comme une forme d'accès rapide et incontrôlé aux "pools d'instances par thread" n'est pas une décision à prendre à la légère.

REMARQUE: il existe d'autres utilisations de ThreadLocal que les "pools d'objets", et ces leçons ne s'appliquent pas aux scénarios dans lesquels ThreadLocal est uniquement destiné à être défini de manière temporaire de toute façon, ou lorsqu'il existe un véritable état par thread à conserver piste de.

Conséquences pour les implémenteurs de bibliothèque

Il y a quelques conséquences pour les implémenteurs de bibliothèques (même lorsque ces bibliothèques sont de simples classes utilitaires dans votre projet).

Soit:

  1. Vous utilisez ThreadLocal, sachant parfaitement que vous pouvez "polluer" les threads de longue durée avec des bagages supplémentaires. Si vous implémentez Java.util.concurrent.ThreadLocalRandom, Cela peut être approprié. (Tomcat peut encore pleurnicher sur les utilisateurs de votre bibliothèque, si vous n'implémentez pas dans Java.*). Il est intéressant de noter la discipline avec laquelle Java.* Utilise avec parcimonie la technique ThreadLocal.

OR

  1. Vous utilisez ThreadLocal et donnez aux clients de votre classe/package: a) la possibilité de choisir de renoncer à cette optimisation ("n'utilisez pas ThreadLocal ... je ne peux pas organiser le nettoyage"); ET b) un moyen de nettoyer les ressources ThreadLocal ("c'est OK d'utiliser ThreadLocal ... Je peux arranger tous les Threads qui vous ont utilisé pour appeler LibClass.releaseThreadLocalsForThread() quand j'en ai fini avec eux.

Rend votre bibliothèque "difficile à utiliser correctement", cependant.

OR

  1. Vous donnez à vos clients la possibilité de fournir leur propre implémentation de pool d'objets (qui peut utiliser ThreadLocal ou une synchronisation quelconque). ("OK, je peux vous donner une new ExpensiveObjectFactory<T>() { public T get() {...} } si vous pensez que c'est vraiment nécessaire").

Pas si mal. Si l'objet est vraiment si important et si cher à créer, la mise en commun explicite en vaut probablement la peine.

OR

  1. Vous décidez que cela ne vaut pas grand-chose de toute façon pour votre application et trouvez une manière différente d'aborder le problème. Ces objets coûteux à créer, modifiables et non threadsafe vous causent de la douleur ... les utiliser est-il vraiment la meilleure option de toute façon?

Alternatives

  1. Mise en commun régulière des objets, avec toute sa synchronisation contestée.
  2. Pas de regroupement d'objets - instanciez-les simplement dans une portée locale et supprimez-les plus tard.
  3. Ne pas regrouper les threads (sauf si vous pouvez planifier le code de nettoyage quand vous le souhaitez) - n'utilisez pas vos trucs dans un conteneur JaveEE
  4. Des pools de threads qui sont suffisamment intelligents pour nettoyer ThreadLocals sans se plaindre.
  5. Pools de threads qui allouent les threads "par application", puis les laissent mourir lorsque l'application est arrêtée.
  6. Un protocole entre les conteneurs de pool de threads et les applications qui a permis l'enregistrement d'un `` gestionnaire d'arrêt d'application '', que le conteneur pourrait planifier pour s'exécuter sur les threads qui avaient été utilisés pour desservir l'application ... à un moment donné dans le futur lorsque ce thread était disponible prochainement. Par exemple. servletContext.addThreadCleanupHandler(new Handler() {@Override cleanup() {...}})

Ce serait bien de voir une certaine normalisation autour des 3 derniers éléments, dans les futures spécifications JavaEE.

Bootnote

En fait, l'instanciation d'un GregorianCalendar est assez légère. C'est l'appel inévitable à setTime() qui entraîne la majeure partie du travail. Il ne contient pas non plus d'état significatif entre les différents points de l'exécution d'un thread. Mettre un Calendar dans un ThreadLocal ne vous rapportera probablement pas plus qu'il ne vous en coûte ... à moins que le profilage ne montre définitivement un point chaud dans new GregorianCalendar().

new SimpleDateFormat(String) est cher en comparaison, car il doit analyser la chaîne de format. Une fois analysé, l '"état" de l'objet est significatif pour les utilisations ultérieures par le même thread. C'est un meilleur ajustement. Mais il pourrait toujours être "moins cher" d'instancier une nouvelle que de donner à vos classes des responsabilités supplémentaires.

33
David Bullock

Étant donné que le fil n'a pas été créé par vous, il a seulement été loué par vous, je pense qu'il est juste d'exiger de le nettoyer avant de cesser de l'utiliser - tout comme vous remplissez le réservoir d'une voiture de location au retour. Tomcat pourrait simplement tout nettoyer lui-même, mais cela vous rend service, rappelant les choses oubliées.

ADD: La façon dont vous utilisez GregorianCalendar préparé est tout simplement erronée: puisque les demandes de service peuvent être simultanées et qu'il n'y a pas de synchronisation, doCalc peut prendre getTime ater setTime invoqué par une autre demande . L'introduction de la synchronisation ralentirait les choses, de sorte que la création d'un nouveau GregorianCalendar pourrait être une meilleure option.

En d'autres termes, votre question devrait être: comment conserver le pool d'instances GregorianCalendar préparées de sorte que son nombre soit ajusté au taux de demande. Donc, au minimum, vous avez besoin d'un singleton qui contient ce pool. Chaque conteneur Ioc a les moyens de gérer un singleton, et la plupart ont des implémentations de pool d'objets prêtes. Si vous n'utilisez pas encore de conteneur IoC, commencez à en utiliser un (String, Guice), plutôt que de réinventer la roue.

4
Alexei Kaigorodov

Si son aide, j'utilise un SPI (une interface) personnalisé et le JDK ServiceLoader. Ensuite, toutes mes diverses bibliothèques internes (jars) qui doivent faire le déchargement de threadlocals juste suivez le modèle ServiceLoader. Donc, si un pot a besoin d'un nettoyage de threadlocal, il sera automatiquement sélectionné s'il a le /META-INF/services/interface.name.

Ensuite, je fais le déchargement dans un filtre ou un écouteur (j'ai eu quelques problèmes avec les écouteurs mais je ne me souviens pas quoi).

Ce serait idéal si le JDK/JEE était livré avec un standard SPI pour effacer les threads locaux.

1
Adam Gent

Je pense que ThreadPoolExecutor de JDK pourrait nettoyer ThreadLocals après l'exécution de la tâche, mais comme nous le savons, ce n'est pas le cas. Je pense que cela aurait pu fournir au moins une option. La raison pourrait être que Thread ne fournit qu'un accès privé au package à ses cartes TreadLocal, de sorte que ThreadPoolExecutor ne peut tout simplement pas y accéder sans modifier l'API de Thread.

Fait intéressant, ThreadPoolExecutor a protégé les stubs de méthode beforeExecution et afterExecution, l'API dit: These can be used to manipulate the execution environment; for example, reinitializing ThreadLocals.... Je peux donc imaginer une tâche qui implémente une interface ThreadLocalCleaner et notre ThreadPoolExecutor personnalisé qui, après l'exécution, appelle cleanThreadLocals () de la tâche;

0
Evgeniy Dorofeev

Après y avoir réfléchi pendant un an, j'ai décidé qu'il n'est pas acceptable qu'un conteneur JavaEE partage des threads de travail regroupés entre des instances d'applications non liées. Ce n'est pas du tout une "entreprise".

Si vous allez vraiment partager des discussions, Java.lang.Thread (Dans un environnement JavaEE, au moins) devrait prendre en charge des méthodes telles que setContextState(int key) et forgetContextState(int key) (mise en miroir setClasLoaderContext()), qui permet au conteneur d'isoler l'état ThreadLocal spécifique à l'application, car il transfère le thread entre diverses applications.

En attendant de telles modifications dans l'espace de noms Java.lang, Il est logique que les déployeurs d'applications adoptent une règle "un pool de threads, une instance d'applications associées" et que les développeurs d'applications supposent que "ce thread est le mien jusqu'à ce que ThreadDeath nous part ".

0
David Bullock