Si j'ai un objet qui est partagé entre les threads, il me semble que chaque champ doit être soit final
ou volatile
, avec le raisonnement suivant:
si le champ doit être modifié (pointer vers un autre objet, mettre à jour la valeur primitive), alors le champ doit être volatile
pour que tous les autres threads opèrent sur la nouvelle valeur. Une simple synchronisation sur les méthodes qui accèdent audit champ est insuffisante car elles pourraient renvoyer une valeur mise en cache.
si le champ ne doit jamais changer, faites-le final
.
Cependant, je n'ai rien trouvé à ce sujet, alors je me demande si cette logique est défectueuse ou tout simplement trop évidente?
[~ # ~] éditez [~ # ~] bien sûr au lieu de volatile on pourrait utiliser un final AtomicReference
ou similaire.
[~ # ~] éditez [~ # ~] par exemple, voir La méthode getter est-elle une alternative à volatile en Java?
[~ # ~] éditez [~ # ~] pour éviter les confusions: Cette question concerne l'invalidation du cache! Si deux threads opèrent sur le même objet, les champs des objets peuvent être mis en cache (par thread), s'ils ne sont pas déclarés volatils. Comment puis-je garantir que le cache est correctement invalidé?
FINAL EDIT Merci à @Peter Lawrey qui m'a indiqué JLS §17 (modèle de mémoire Java). Pour autant que je vois, il indique que la synchronisation établit une relation passe-avant entre les opérations, de sorte qu'un thread voit les mises à jour d'un autre thread si ces mises à jour se sont produites avant, par exemple si getter et setter pour un champ non volatile sont synchronized
.
Bien que je pense que private final
Aurait probablement dû être la valeur par défaut pour les champs et les variables avec un mot-clé comme var
le rendant mutable, utiliser volatile lorsque vous n'en avez pas besoin est
final
qui améliore la clarté en disant que cela ne devrait pas être modifié, utiliser volatile
quand il n'est pas nécessaire, est susceptible de prêter à confusion car le lecteur essaie de comprendre pourquoi il a été rendu volatil.si le champ doit être modifié (pointer vers un autre objet, mettre à jour la valeur primitive), alors le champ doit être volatile pour que tous les autres threads opèrent sur la nouvelle valeur.
Bien que cela soit bien pour les lectures, considérez ce cas trivial.
volatile int x;
x++;
Ce n'est pas thread-safe. Comme c'est la même chose que
int x2 = x;
x2 = x2 + 1; // multiple threads could be executing on the same value at this point.
x = x2;
Le pire est que l'utilisation de volatile
rendrait ce type de bogue plus difficile à trouver.
Comme le souligne yshavit, la mise à jour de plusieurs champs est plus difficile à contourner avec volatile
par exemple HashMap.put(a, b)
met à jour plusieurs références.
Une simple synchronisation sur les méthodes qui accèdent audit champ est insuffisante car elles pourraient renvoyer une valeur mise en cache.
synchronized vous offre toutes les garanties de mémoire de volatile
et plus, c'est pourquoi il est considérablement plus lent.
REMARQUE: juste synchronized
- chaque méthode n'est pas toujours suffisante non plus. StringBuffer
a toutes les méthodes synchronisées mais est pire qu'inutile dans un contexte multi-thread car son utilisation est susceptible d'être source d'erreurs.
Il est trop facile de supposer que la sécurité du fil est comme saupoudrer de poussière de fée, ajoutez un peu de sécurité au fil magique et vos bugs disparaissent. Le problème est que la sécurité du fil ressemble plus à un seau avec de nombreux trous. Bouchez les plus gros trous et les bugs peuvent sembler disparaître, mais à moins de tous les boucher, vous n'avez pas la sécurité des threads, mais cela peut être plus difficile à trouver.
En termes de synchronisation vs volatile, cela indique
D'autres mécanismes, tels que les lectures et les écritures de variables volatiles et l'utilisation de classes dans le package Java.util.concurrent, offrent d'autres moyens de synchronisation.
https://docs.Oracle.com/javase/specs/jls/se7/html/jls-17.html
Faire des champs que vous n'avez pas besoin de modifier final
est une bonne idée, indépendamment des problèmes de threading. Cela rend les instances de la classe plus faciles à raisonner, car vous pouvez savoir plus facilement dans quel état elles se trouvent.
En termes de rendre les autres champs volatile
:
Une simple synchronisation sur les méthodes qui accèdent audit champ est insuffisante car elles pourraient renvoyer une valeur mise en cache.
Vous ne verriez une valeur en cache que si vous accédez à la valeur en dehors d'un bloc synchronisé.
Tous les accès devraient être correctement synchronisés. La fin d'un bloc synchronisé est garantie avant le début d'un autre bloc synchronisé (lors de la synchronisation sur le même moniteur).
Il y a au moins quelques cas où vous auriez encore besoin d'utiliser la synchronisation:
Atomic*
classe au lieu d'un "ancien champ simple"; mais même pour une seule mise à jour de champ, vous pouvez toujours avoir besoin d'un accès exclusif (par exemple, ajouter un élément à une liste tout en supprimant un autre).ArrayList
ou un tableau.Si un objet est partagé entre des threads, vous avez deux options claires:
1. Rendre cet objet en lecture seule
Les mises à jour (ou cache) n'ont donc aucun impact.
2. Synchroniser sur l'objet lui-même
L'invalidation du cache est difficile. Très difficile. Donc, si vous ne devez garantir aucune valeur périmée, vous devez protéger cette valeur et protéger le verrou autour de cette valeur.
Rendez le verrou et les valeurs privés sur l'objet partagé, les opérations sont donc un détail d'implémentation.
Pour éviter les verrous morts, ces opérations doivent être aussi "atomiques" que pour éviter d'interagir avec les autres verrous.