From Head First design patterns book, le pattern singleton avec double verrouillage a été implémenté comme suit:
public class Singleton {
private volatile static Singleton instance;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
Je ne comprends pas pourquoi volatile
est utilisé. L’utilisation de volatile
ne va-t-elle pas à l’encontre de l’utilisation du verrouillage à double contrôle, c’est-à-dire des performances?
Une bonne ressource pour comprendre pourquoi volatile
est nécessaire provient de JCIP book. Wikipedia a une explication décente de ce matériel.
Le vrai problème est que Thread A
peut affecter un espace mémoire à instance
avant la fin de la construction de instance
. Thread B
verra cette assignation et essaiera de l'utiliser. Il en résulte que Thread B
échoue car il utilise une version partiellement construite de instance
.
Comme indiqué par @irreputable, la technologie volatile n’est pas chère. Même si cela coûte cher, la cohérence doit être prioritaire sur la performance.
Il existe encore une manière élégante et propre pour les Lazy Singletons.
public final class Singleton {
private Singleton() {}
public static Singleton getInstance() {
return LazyHolder.INSTANCE;
}
private static class LazyHolder {
private static final Singleton INSTANCE = new Singleton();
}
}
Article d'origine: Initialization-on-demand_holder_idiom de wikipedia
En génie logiciel, l'idiome initialisation sur support (motif de conception) est un singleton chargé paresseux. Dans toutes les versions de Java, l’idiome permet une initialisation paresseuse sécurisée, hautement concurrente et offrant de bonnes performances.
Comme la classe n'a aucune variable static
à initialiser, l'initialisation se termine de manière triviale.
La définition de classe statique LazyHolder
qu’elle contient n’est initialisée que lorsque la machine virtuelle Java détermine que LazyHolder doit être exécuté.
La classe statique LazyHolder
n'est exécutée que lorsque la méthode statique getInstance
est invoquée sur la classe Singleton, et la première fois que cela se produit, la machine virtuelle Java charge et initialise la classe LazyHolder
.
Cette solution est thread-safe sans nécessiter de construction de langage spéciale (c'est-à-dire volatile
ou synchronized
).
Eh bien, il n'y a pas de double vérification de verrouillage pour la performance. C'est un cassé motif.
Laissant de côté les émotions, volatile
est ici, car sans lui au moment où le deuxième thread passe instance == null
, le premier thread ne peut pas encore construire new Singleton()
: personne ne promet que la création de l'objet arrive-avant - affectation à instance
pour un autre thread créer réellement l'objet.
volatile
établit à son tour arrive-avant - la relation entre les lectures et les écritures, et corrige le modèle interrompu.
Si vous recherchez des performances, utilisez plutôt la classe statique interne du support.
Si vous ne l'aviez pas, un deuxième thread pourrait entrer dans le bloc synchronisé après le premier, définissez-le sur null et votre cache local penserait toujours qu'il est null.
Le premier n’est pas pour l’exactitude (si c’est le cas, cela irait à l’encontre de soi-même), mais pour l’optimisation.
Une lecture volatile n'est pas vraiment chère en soi.
Vous pouvez concevoir un test pour appeler getInstance()
dans une boucle serrée, afin d’observer l’impact d’une lecture volatile; Cependant, ce test n'est pas réaliste. Dans une telle situation, le programmeur appelle habituellement getInstance()
une fois et met en cache l'instance pour la durée d'utilisation.
Une autre implication consiste à utiliser un champ final
(voir wikipedia). Cela nécessite une lecture supplémentaire, qui peut devenir plus onéreuse que la version volatile
. La version final
peut être plus rapide dans une boucle serrée, mais ce test est sans objet, comme indiqué précédemment.
Déclarer la variable en tant que volatile
garantit que tous les accès à celle-ci lisent réellement sa valeur actuelle de la mémoire.
Sans volatile
, le compilateur peut optimiser les accès en mémoire et conserver sa valeur dans un registre, de sorte que seule la première utilisation de la variable lise l'emplacement réel de la mémoire contenant la variable. Cela pose un problème si la variable est modifiée par un autre thread entre le premier et le deuxième accès. le premier thread n'a qu'une copie de la première valeur (pré-modifiée), de sorte que la deuxième instruction if
teste une copie périmée de la valeur de la variable.