Votre idiome est en sécurité si et seulement si la référence à HashMap
est publiée en toute sécurité . Plutôt que tout ce qui concerne les éléments internes d' HashMap
elle-même, la publication sécurisée traite de la façon dont le thread de construction rend la référence à la carte visible aux autres threads.
Fondamentalement, la seule course possible ici se situe entre la construction du HashMap
et les fils de lecture qui peuvent y accéder avant qu'il ne soit entièrement construit. La majeure partie de la discussion porte sur ce qui arrive à l'état de l'objet de la carte, mais cela n'a pas d'importance puisque vous ne le modifiez jamais - donc la seule partie intéressante est la façon dont la HashMap
référence est publiée.
Par exemple, imaginez que vous publiez la carte comme ceci:
class SomeClass {
public static HashMap<Object, Object> MAP;
public synchronized static setMap(HashMap<Object, Object> m) {
MAP = m;
}
}
... et à un moment donné setMap()
est appelé avec une carte, et d'autres threads utilisent SomeClass.MAP
pour accéder à la carte, et vérifiez la valeur null comme ceci:
HashMap<Object,Object> map = SomeClass.MAP;
if (map != null) {
.. use the map
} else {
.. some default behavior
}
Ce n'est pas sûr même si cela semble probablement le cas. Le problème est qu'il n'y a pas se produit, avant la relation entre l'ensemble des SomeObject.MAP
et la lecture ultérieure sur un autre fil, de sorte que le fil de lecture est libre de voir une carte partiellement construite. Cela peut faire n'importe quoi et même en pratique, cela fait des choses comme mettre le fil de lecture dans une boucle infinie .
Pour publier la carte en toute sécurité, vous devez établir une relation d' avance entre l' écriture de la référence à la HashMap
(c.-à-d. La publication ) et les lecteurs suivants de cette référence (c.-à-d. La consommation). Idéalement, il n'y a que quelques - uns faciles à retenir les moyens d' y parvenir que [1] :
- Échangez la référence via un champ correctement verrouillé ( JLS 17.4.5 )
- Utilisez l'initialiseur statique pour effectuer l'initialisation des magasins ( JLS 12.4 )
- Echangez la référence via un champ volatil ( JLS 17.4.5 ), ou en conséquence de cette règle, via les classes AtomicX
- Initialisez la valeur dans un champ final ( JLS 17.5 ).
Les plus intéressants pour votre scénario sont (2), (3) et (4). En particulier, (3) s'applique directement au code que j'ai ci-dessus: si vous transformez la déclaration de MAP
en:
public static volatile HashMap<Object, Object> MAP;
alors tout est casher: les lecteurs qui voient une non nulle valeur ont nécessairement se passe-avant relation avec le magasin pour MAP
et donc voir tous les magasins associés à l'initialisation de la carte.
Les autres méthodes changent la sémantique de votre méthode, puisque les deux (2) (en utilisant l'initaliseur statique) et (4) (en utilisant final ) impliquent que vous ne pouvez pas définir MAP
dynamiquement au moment de l'exécution. Si vous n'avez pas besoin de le faire, déclarez simplement en MAP
tant que static final HashMap<>
et vous êtes assuré d'une publication sécurisée.
En pratique, les règles sont simples pour un accès sécurisé aux "objets jamais modifiés":
Si vous publiez un objet qui n'est pas intrinsèquement immuable (comme dans tous les champs déclarés final
) et:
- Vous pouvez déjà créer l'objet qui sera affecté au moment de la déclaration a : il suffit d'utiliser un
final
champ (y compris static final
pour les membres statiques).
- Vous souhaitez affecter l'objet ultérieurement, après que la référence soit déjà visible: utilisez un champ volatile b .
C'est tout!
En pratique, c'est très efficace. L'utilisation d'un static final
champ, par exemple, permet à la JVM de supposer que la valeur est inchangée pendant la durée de vie du programme et de l'optimiser fortement. L'utilisation d'un final
champ membre permet à la plupart des architectures de lire le champ d'une manière équivalente à une lecture de champ normale et n'empêche pas d'autres optimisations c .
Enfin, l'utilisation de volatile
a un certain impact: aucune barrière matérielle n'est nécessaire sur de nombreuses architectures (telles que x86, en particulier celles qui ne permettent pas aux lectures de passer des lectures), mais une optimisation et une réorganisation peuvent ne pas se produire au moment de la compilation - mais cela l'effet est généralement faible. En échange, vous obtenez en fait plus que ce que vous avez demandé - non seulement vous pouvez en publier un en toute sécurité HashMap
, mais vous pouvez stocker autant de HashMap
s non modifiés que vous le souhaitez sur la même référence et être assuré que tous les lecteurs verront une carte publiée en toute sécurité. .
Pour plus de détails sanglants, reportez-vous à Shipilev ou à cette FAQ de Manson et Goetz .
[1] Citation directe de shipilev .
a Cela semble compliqué, mais ce que je veux dire, c'est que vous pouvez attribuer la référence au moment de la construction - soit au point de déclaration, soit dans le constructeur (champs membres) ou l'initialiseur statique (champs statiques).
b En option, vous pouvez utiliser une synchronized
méthode pour obtenir / définir, ou un AtomicReference
ou quelque chose, mais nous parlons du travail minimum que vous pouvez faire.
c Certaines architectures avec des modèles de mémoire très faibles (je regarde vous , Alpha) peuvent nécessiter un certain type de barrière de lecture avant une final
lecture - mais ceux - ci sont très rares aujourd'hui.