JavaRush /Blog Java /Random-FR /Gérer la volatilité
lexmirnov
Niveau 29
Москва

Gérer la volatilité

Publié dans le groupe Random-FR

Lignes directrices pour l'utilisation de variables volatiles

Par Brian Goetz 19 juin 2007 Original : Gestion de la volatilité Les variables volatiles en Java peuvent être appelées « lumière synchronisée » ; Ils nécessitent moins de code que les blocs synchronisés, s’exécutent souvent plus rapidement, mais ne peuvent faire qu’une fraction de ce que font les blocs synchronisés. Cet article présente plusieurs modèles d'utilisation efficace de volatile et quelques avertissements sur les endroits où ne pas l'utiliser. Les verrous ont deux caractéristiques principales : l'exclusion mutuelle (mutex) et la visibilité. L'exclusion mutuelle signifie qu'un verrou ne peut être détenu que par un seul thread à la fois, et cette propriété peut être utilisée pour implémenter des protocoles de contrôle d'accès aux ressources partagées afin qu'un seul thread les utilise à la fois. La visibilité est un problème plus subtil, son objectif est de garantir que les modifications apportées aux ressources publiques avant la libération du verrou seront visibles par le prochain thread qui prendra en charge ce verrou. Si la synchronisation ne garantissait pas la visibilité, les threads pourraient recevoir des valeurs obsolètes ou incorrectes pour les variables publiques, ce qui entraînerait un certain nombre de problèmes graves.
Variables volatiles
Les variables volatiles ont les propriétés de visibilité des variables synchronisées, mais n'ont pas leur atomicité. Cela signifie que les threads utiliseront automatiquement les valeurs les plus récentes des variables volatiles. Ils peuvent être utilisés pour la sécurité des threads , mais dans un ensemble de cas très limité : ceux qui n'introduisent pas de relations entre plusieurs variables ou entre les valeurs actuelles et futures d'une variable. Ainsi, volatile seul ne suffit pas pour implémenter un compteur, un mutex ou toute classe dont les parties immuables sont associées à plusieurs variables (par exemple, "start <=end"). Vous pouvez choisir des verrous volatiles pour l’une des deux raisons principales : la simplicité ou l’évolutivité. Certaines constructions de langage sont plus faciles à écrire sous forme de code de programme, puis à lire et à comprendre lorsqu'elles utilisent des variables volatiles au lieu de verrous. De plus, contrairement aux verrous, ils ne peuvent pas bloquer un thread et sont donc moins sujets aux problèmes d’évolutivité. Dans les situations où il y a beaucoup plus de lectures que d'écritures, les variables volatiles peuvent offrir des avantages en termes de performances par rapport aux verrous.
Conditions d'utilisation correcte des volatiles
Vous pouvez remplacer les verrous par des verrous volatiles dans un nombre limité de circonstances. Pour être thread-safe, les deux critères doivent être remplis :
  1. Ce qui est écrit dans une variable est indépendant de sa valeur actuelle.
  2. La variable ne participe pas aux invariants avec d'autres variables.
En termes simples, ces conditions signifient que les valeurs valides pouvant être écrites dans une variable volatile sont indépendantes de tout autre état du programme, y compris l'état actuel de la variable. La première condition exclut l'utilisation de variables volatiles comme compteurs thread-safe. Bien que l'incrément (x++) ressemble à une opération unique, il s'agit en fait d'une séquence complète d'opérations de lecture-modification-écriture qui doivent être effectuées de manière atomique, ce que volatile ne fournit pas. Une opération valide nécessiterait que la valeur de x reste la même tout au long de l'opération, ce qui ne peut pas être obtenu en utilisant volatile. (Cependant, si vous pouvez garantir que la valeur est écrite à partir d'un seul thread, la première condition peut être omise.) Dans la plupart des situations, la première ou la deuxième condition sera violée, ce qui fait des variables volatiles une approche moins couramment utilisée pour assurer la sécurité des threads que les variables synchronisées. Le listing 1 montre une classe non thread-safe avec une plage de nombres. Il contient un invariant : la limite inférieure est toujours inférieure ou égale à la limite supérieure. @NotThreadSafe public class NumberRange { private int lower, upper; public int getLower() { return lower; } public int getUpper() { return upper; } public void setLower(int value) { if (value > upper) throw new IllegalArgumentException(...); lower = value; } public void setUpper(int value) { if (value < lower) throw new IllegalArgumentException(...); upper = value; } } Étant donné que les variables d'état de plage sont limitées de cette manière, il ne suffira pas de rendre les champs inférieur et supérieur volatiles pour garantir que la classe est thread-safe ; la synchronisation sera toujours nécessaire. Sinon, tôt ou tard, vous n'aurez pas de chance et deux threads exécutant setLower() et setUpper() avec des valeurs inappropriées peuvent conduire la plage à un état incohérent. Par exemple, si la valeur initiale est (0, 5), que le thread A appelle setLower(4) et qu'en même temps le thread B appelle setUpper(3), ces opérations entrelacées entraîneront une erreur, même si les deux passeront le contrôle. cela est censé protéger l’invariant. En conséquence, la plage sera (4, 3) - valeurs incorrectes. Nous devons rendre setLower() et setUpper() atomiques par rapport aux autres opérations de plage - et rendre les champs volatils ne fera pas cela.
Considérations relatives aux performances
La première raison d’utiliser volatile est la simplicité. Dans certaines situations, utiliser une telle variable est tout simplement plus simple que d'utiliser le verrou qui lui est associé. La deuxième raison est la performance, parfois volatile fonctionnera plus rapidement que les verrous. Il est extrêmement difficile de formuler des déclarations précises et globales telles que « X est toujours plus rapide que Y », en particulier lorsqu'il s'agit des opérations internes de la machine virtuelle Java. (Par exemple, la JVM peut libérer entièrement le verrou dans certaines situations, ce qui rend difficile la discussion abstraite des coûts de la volatilité par rapport à la synchronisation). Cependant, sur la plupart des architectures de processeurs modernes, le coût de lecture des variables volatiles n'est pas très différent du coût de la lecture des variables régulières. Le coût de l'écriture de variables volatiles est nettement plus élevé que celui de l'écriture de variables régulières en raison de la séparation de la mémoire requise pour la visibilité, mais il est généralement moins cher que la définition de verrous.
Modèles pour une utilisation appropriée de volatile
De nombreux experts en concurrence ont tendance à éviter complètement d’utiliser des variables volatiles, car elles sont plus difficiles à utiliser correctement que les verrous. Cependant, il existe certains modèles bien définis qui, s’ils sont suivis attentivement, peuvent être utilisés en toute sécurité dans une grande variété de situations. Respectez toujours les limitations de volatile - utilisez uniquement des volatiles indépendants de tout autre élément du programme, ce qui devrait vous empêcher d'entrer dans un territoire dangereux avec ces modèles.
Modèle n° 1 : indicateurs d'état
L'utilisation canonique de variables mutables consiste peut-être en de simples indicateurs d'état booléens indiquant qu'un événement ponctuel important du cycle de vie s'est produit, tel que la fin de l'initialisation ou une demande d'arrêt. De nombreuses applications incluent une construction de contrôle de la forme : "jusqu'à ce que nous soyons prêts à arrêter, continuez à fonctionner", comme indiqué dans le Listing 2 : Il est volatile boolean shutdownRequested; ... public void shutdown() { shutdownRequested = true; } public void doWork() { while (!shutdownRequested) { // do stuff } } probable que la méthode shutdown() sera appelée quelque part en dehors de la boucle - sur un autre thread - la synchronisation est donc nécessaire pour garantir une visibilité correcte des variables shutdownRequested. (Il peut être appelé depuis un écouteur JMX, un écouteur d'action dans un fil d'événement GUI, via RMI, via un service web, etc.). Cependant, une boucle avec des blocs synchronisés sera beaucoup plus lourde qu'une boucle avec un indicateur d'état volatile comme dans le listing 2. Parce que volatile facilite l'écriture du code et que l'indicateur d'état ne dépend d'aucun autre état du programme, ceci est un exemple d'une boucle avec des blocs synchronisés. bon usage du volatile. La caractéristique de ces indicateurs d’état est qu’il n’y a généralement qu’une seule transition d’état ; l'indicateur shutdownRequested passe de faux à vrai, puis le programme s'arrête. Ce modèle peut être étendu aux drapeaux d’état qui peuvent changer d’avant en arrière, mais seulement si le cycle de transition (de faux à vrai à faux) se produit sans intervention extérieure. Sinon, une sorte de mécanisme de transition atomique, tel que des variables atomiques, est nécessaire.
Modèle n°2 : publication sécurisée unique
Les erreurs de visibilité possibles en l'absence de synchronisation peuvent devenir un problème encore plus difficile lors de l'écriture de références d'objet au lieu de valeurs primitives. Sans synchronisation, vous pouvez voir la valeur actuelle d'une référence d'objet qui a été écrite par un autre thread et toujours voir les valeurs d'état obsolètes pour cet objet. (Cette menace est à l'origine du problème du fameux verrou à double vérification, dans lequel une référence d'objet est lue sans synchronisation, et vous risquez de voir la référence réelle mais de faire passer un objet partiellement construit à travers elle.) Une façon de publier en toute sécurité un object est de faire une référence à un objet volatile. Le listing 3 montre un exemple dans lequel, lors du démarrage, un thread en arrière-plan charge certaines données de la base de données. Un autre code susceptible d'essayer d'utiliser ces données vérifie si elles ont été publiées avant d'essayer de les utiliser. public class BackgroundFloobleLoader { public volatile Flooble theFlooble; public void initInBackground() { // делаем много всякого theFlooble = new Flooble(); // единственная запись в theFlooble } } public class SomeOtherClass { public void doWork() { while (true) { // чё-то там делаем... // используем theFolooble, но только если она готова if (floobleLoader.theFlooble != null) doSomething(floobleLoader.theFlooble); } } } Si la référence à theFlooble n'était pas volatile, le code dans doWork() risquerait de voir un Flooble partiellement construit lorsqu'il tenterait de référencer theFlooble. L'exigence clé de ce modèle est que l'objet publié doit être soit thread-safe, soit effectivement immuable (effectivement immuable signifie que son état ne change jamais après sa publication). Un lien volatile peut garantir qu'un objet est visible dans sa forme publiée, mais si l'état de l'objet change après la publication, une synchronisation supplémentaire est requise.
Modèle n°3 : observations indépendantes
Un autre exemple simple d'utilisation sûre de volatile est lorsque les observations sont périodiquement « publiées » pour être utilisées dans un programme. Par exemple, il existe un capteur environnemental qui détecte la température actuelle. Le thread d'arrière-plan peut lire ce capteur toutes les quelques secondes et mettre à jour une variable volatile contenant la température actuelle. D'autres threads peuvent alors lire cette variable, sachant que la valeur qu'elle contient est toujours à jour. Une autre utilisation de ce modèle consiste à collecter des statistiques sur le programme. Le listing 4 montre comment le mécanisme d'authentification peut mémoriser le nom du dernier utilisateur connecté. La référence lastUser sera réutilisée pour publier la valeur à utiliser par le reste du programme. public class UserManager { public volatile String lastUser; public boolean authenticate(String user, String password) { boolean valid = passwordIsValid(user, password); if (valid) { User u = new User(); activeUsers.add(u); lastUser = user; } return valid; } } Ce modèle s’étend au précédent ; la valeur est publiée pour être utilisée ailleurs dans le programme, mais la publication n'est pas un événement ponctuel, mais une série d'événements indépendants. Ce modèle nécessite que la valeur publiée soit effectivement immuable, c'est-à-dire que son état ne change pas après la publication. Le code utilisant la valeur doit être conscient qu'elle peut changer à tout moment.
Modèle n°4 : modèle « haricot volatile »
Le modèle « haricot volatile » est applicable dans les frameworks qui utilisent des JavaBeans comme « structures glorifiées ». Le modèle « haricot volatile » utilise un JavaBean comme conteneur pour un groupe de propriétés indépendantes avec des getters et/ou des setters. La raison d'être du modèle « haricot volatile » est que de nombreux frameworks fournissent des conteneurs pour les détenteurs de données mutables (tels que HttpSession), mais les objets placés dans ces conteneurs doivent être thread-safe. Dans le modèle de bean volatile, tous les éléments de données JavaBean sont volatils, et les getters et setters doivent être triviaux - ils ne doivent contenir aucune logique autre que l'obtention ou la définition de la propriété correspondante. De plus, pour les données membres qui sont des références d’objets, lesdits objets doivent être effectivement immuables. (Cela interdit les champs de référence de tableau, car lorsqu'une référence de tableau est déclarée volatile, seule cette référence, et non les éléments eux-mêmes, possède la propriété volatile.) Comme pour toute variable volatile, il ne peut y avoir aucun invariant ou restriction associé aux propriétés des JavaBeans. . Un exemple de JavaBean écrit en utilisant le modèle « volatile bean » est présenté dans le listing 5 : @ThreadSafe public class Person { private volatile String firstName; private volatile String lastName; private volatile int age; public String getFirstName() { return firstName; } public String getLastName() { return lastName; } public int getAge() { return age; } public void setFirstName(String firstName) { this.firstName = firstName; } public void setLastName(String lastName) { this.lastName = lastName; } public void setAge(int age) { this.age = age; } }
Modèles volatils plus complexes
Les modèles de la section précédente couvrent la plupart des cas courants où l'utilisation de volatile est raisonnable et évidente. Cette section examine un modèle plus complexe dans lequel volatile peut offrir un avantage en termes de performances ou d'évolutivité. Les modèles volatils plus avancés peuvent être extrêmement fragiles. Il est essentiel que vos hypothèses soient soigneusement documentées et que ces modèles soient solidement encapsulés, car même les plus petits changements peuvent casser votre code ! De plus, étant donné que la principale raison des cas d'utilisation volatiles plus complexes est la performance, assurez-vous que vous avez réellement besoin du gain de performance souhaité avant de les utiliser. Ces modèles sont des compromis qui sacrifient la lisibilité ou la facilité de maintenabilité au profit d'éventuels gains de performances - si vous n'avez pas besoin d'amélioration des performances (ou si vous ne pouvez pas prouver que vous en avez besoin avec un programme de mesure rigoureux), alors c'est probablement une mauvaise affaire car cela vous abandonnez quelque chose de précieux et obtenez quelque chose de moins en retour.
Modèle n°5 : Verrouillage en lecture-écriture bon marché
Vous devez maintenant savoir que volatile est trop faible pour implémenter un compteur. Puisque ++x est essentiellement une réduction de trois opérations (lecture, ajout, stockage), si les choses tournent mal, vous perdrez la valeur mise à jour si plusieurs threads tentent d'incrémenter le compteur volatile en même temps. Toutefois, s'il y a beaucoup plus de lectures que de modifications, vous pouvez combiner le verrouillage intrinsèque et les variables volatiles pour réduire la surcharge globale du chemin de code. Le listing 6 montre un compteur thread-safe qui utilise synchronisé pour garantir que l'opération d'incrémentation est atomique et utilise volatile pour garantir que le résultat actuel est visible. Si les mises à jour sont peu fréquentes, cette approche peut améliorer les performances puisque les coûts de lecture sont limités aux lectures volatiles, qui sont généralement moins chères que l'acquisition d'un verrou non conflictuel. @ThreadSafe public class CheesyCounter { // Employs the cheap read-write lock trick // All mutative operations MUST be done with the 'this' lock held @GuardedBy("this") private volatile int value; public int getValue() { return value; } public synchronized int increment() { return value++; } } La raison pour laquelle cette méthode est appelée « verrouillage en lecture-écriture bon marché » est que vous utilisez des mécanismes de synchronisation différents pour les lectures et les écritures. Étant donné que les opérations d'écriture dans ce cas violent la première condition d'utilisation de volatile, vous ne pouvez pas utiliser volatile pour implémenter un compteur en toute sécurité - vous devez utiliser un verrou. Cependant, vous pouvez utiliser volatile pour rendre la valeur actuelle visible lors de la lecture, vous utilisez donc un verrou pour toutes les opérations de modification et volatile pour les opérations en lecture seule. Si un verrou autorise uniquement un thread à la fois à accéder à une valeur, les lectures volatiles en autorisent plusieurs. Ainsi, lorsque vous utilisez volatile pour protéger la lecture, vous obtenez un niveau d'échange plus élevé que si vous utilisez un verrou sur tout le code : et lit et enregistre. Attention cependant à la fragilité de ce pattern : avec deux mécanismes de synchronisation concurrents, cela peut devenir très complexe si l'on dépasse l'application la plus basique de ce pattern.
Résumé
Les variables volatiles constituent une forme de synchronisation plus simple mais plus faible que le verrouillage, qui dans certains cas offre de meilleures performances ou évolutivité que le verrouillage intrinsèque. Si vous remplissez les conditions d'utilisation sûre de volatile - une variable est véritablement indépendante à la fois des autres variables et de ses propres valeurs précédentes - vous pouvez parfois simplifier le code en remplaçant synchronisé par volatile. Cependant, le code qui utilise le volatile est souvent plus fragile que le code qui utilise le verrouillage. Les modèles suggérés ici couvrent les cas les plus courants où la volatilité est une alternative raisonnable à la synchronisation. En suivant ces modèles - et en prenant soin de ne pas les pousser au-delà de leurs propres limites - vous pouvez utiliser le volatile en toute sécurité dans les cas où ils apportent des avantages.
Commentaires
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION