3
votes

Comment éviter la surcharge de performances liée à l'utilisation d'un modèle volatile dans un singleton?

Dites le code pour le modèle Singleton:

class Singleton 
{ 
    private volatile static Singleton obj; 

    private Singleton() {} 

    public static Singleton getInstance() 
    { 
        if (obj == null) 
        { 
            synchronized (Singleton.class) 
            { 
                if (obj==null) 
                    obj = new Singleton(); 
            } 
        } 
        return obj; 
    } 
} 

obj dans le code ci-dessus est marqué comme volatile, ce qui signifie que chaque fois que obj est utilisé dans le code, il est toujours extrait de la mémoire principale au lieu de en utilisant la valeur mise en cache. Ainsi, chaque fois que if (obj == null) doit être exécuté, il récupère obj de la mémoire principale, bien que sa valeur soit définie lors de l'exécution précédente. Il s'agit d'une surcharge de performance liée à l'utilisation de mots clés volatils. Comment l'éviter?


10 commentaires

évitez la double vérification si vous n'en avez pas besoin.!


Tout d'abord, vous confondez les blocs volatile et synchronisé . Les deux fournissent l ' atomicité (signifie que les modifications apportées par un thread sont visibles par les autres) mais le bloc synchronisé fournit également une exclusion mutuelle , ce qui signifie que cela ne n'autorisez pas plus d'un thread à y accéder à la fois. Mon point est volatile est utilisé lorsque vous voulez l'atomicité mais ne voulez pas l'exclusion mutuelle. Vous ne devriez probablement pas utiliser les deux en combinaison car cela va à l'encontre de l'objectif d'utiliser le mot-clé volatile .


Je pense qu'il veut éviter la double initialisation dans le cas où plusieurs threads trouvent obj nul.


Pour ajouter à @MushifAliNawaz, ce que vous voulez probablement est volatile et non synchronisé . Techniquement, le seul moyen sûr d'implémenter ce getInstance () avec la vérification est d'utiliser le bloc synchronized - si vous souhaitez vous assurer que le constructeur n'est appelé qu'une seule fois . C'est le modèle que vous souhaitez utiliser lorsque la construction est lourde ou ne devrait se produire qu'une seule fois pour d'autres raisons. C'est également correct. Souvent, cependant, vous voulez un seul objet du type pour d'autres raisons - en construire deux est bien, tant qu'un seul est utilisé. Ensuite, ignorez synchronisé et utilisez volatile .


@MushifAliNawaz "Les deux fournissent l'atomicité" - non, cela devrait être "Les deux fournissent la visibilité ", une variable volatile n'est pas atomique. aussi "Vous ne devriez probablement pas utiliser les deux en combinaison car cela va à l'encontre de l'objectif d'utiliser un mot-clé volatile." - c'est comme dire ne pas utiliser le DCL, c'est au moins inexact.


@JensFinkhaeuser votre commentaire est au moins déroutant. si vous implémentiez simplement ce getInstance avec seulement un bloc synchronisé - vous pourriez potentiellement acquérir un verrou pour chaque appel; L'intérêt de DCL n'est pas de faire cela - et de ne le faire qu'une seule fois. Le volatile est un must pour que chaque thread puisse voir l'instance publiée et initialisée.


@Eugene Ouais, tu as raison. Je devrais probablement reformuler atomique en visibilité . Autant que je sache, le mot-clé volatile force l'environnement de traitement à purger immédiatement les modifications de la mémoire principale qui sont apportées aux variables afin que ces modifications soient visibles par tous les threads. Cela ne correspond probablement pas exactement à la définition de atomique .


@MushifAliNawaz probablement ? cela ne correspond pas à un fait. le "vidage" de la mémoire principale est un mythe (bien qu'en pratique il en soit proche - car il vide le tampon de stockage). Les seules garanties sur lesquelles vous pouvez compter sont celles qui sont stipulées dans le JLS pour volatile .


@Eugene bien sûr, le but de DCL est d'essayer d'éviter d'acquérir un verrou. C'est une optimisation, pas plus. L'OP concerne la confusion entre volatile et synchronized , il est donc très peu nécessaire d'intégrer DCL dans l'image. En ce qui concerne volatile étant un must, c'est précisément ce que je veux dire. Ce n'est que si vous avez des exigences très spécifiques que d'innombrables exemples n'ont pas, à savoir que vous instanciez la classe singleton une seule fois. La plupart se contentent d'exigences plus souples, qui peuvent ignorer volatile . Je ne le recommanderais pas, mais c'est possible.


@JensFinkhaeuser même ici dans votre explication, vous mentionnez - La plupart se contentent d'exigences plus lâches, qui peuvent ignorer les volatiles et je me demande ... voulez-vous dire que volatile peut être ignoré cas de DCL? Parce que si vous êtes - vous avez tout simplement tort Si vous voulez dire qu'il existe d'autres moyens d'obtenir un singleton - je serais d'accord. Choisir avec soin des mots dans un domaine aussi sensible est le plus important.


4 Réponses :


0
votes

Si vous voulez éviter d'utiliser volatile, alors vous pouvez initialiser lors du chargement de la classe et utiliser un constructeur privé pour éviter de créer une nouvelle instance.

public class Singleton{
    //Initialized when class loading
    private static final Singleton INSTANCE = new Singleton();

    //To avoid creating new instance of Singleton
    private Singleton(){}

    public static Singleton getSingleton(){
        return INSTANCE;
    }
}


3 commentaires

que se passera-t-il s'il s'agit d'une application distribuée (fonctionnant sur plusieurs processeurs)? Cela créera plusieurs instances distinctes. C'est pourquoi il existe un concept de volatile .


@MushifAliNawaz que voulez-vous dire plusieurs instances séparées? c'est un singleton - un par chargeur de classe. que voulez-vous dire aussi "application distribuée (fonctionnant sur plusieurs processeurs)"?


@MushifAliNawaz, Singleton est censé avoir une instance. Mais cela ne garantit pas qu'il n'y a qu'une seule instance par JVM, cela garantit seulement qu'il y a une instance par classloader. lors de l'utilisation de plusieurs classloaders, chacun a son propre.



0
votes

Vous pouvez utiliser Initialisation différée avec la classe statique Holder

class Singleton 
{ 

    private Singleton() {} 

    private static class LazyLoader{ 
        static final Singleton obj = new Singleton();
    }

    public static Singleton getInstance() 
    { 
        return LazyLoader.obj;
    } 
} 

La chose importante à noter ici est que le constructeur doit être sûr, sinon le chargeur de classe lancera NoClassDefFoundError


0 commentaires

0
votes

Vous devez utiliser Enums pour l'implémentation de Singleton.

Joshua Bloch suggère d'utiliser Enum pour implémenter le modèle de conception Singleton car Java garantira que toute valeur enum n'est instanciée qu'une seule fois dans un Java programme. L'inconvénient est que le type enum est quelque peu inflexible; pour exemple, il n'autorise pas l'initialisation tardive.

public class EnumDemo {
    public static void main(String[] args) {
        EnumSingleton singleton = EnumSingleton.INSTANCE;
        System.out.println(singleton.getValue());
        singleton.setValue(2);
        System.out.println(singleton.getValue());
    }
}
public enum EnumSingleton {
    INSTANCE;
    int value;
    public int getValue() {
        return value;
    }
    public void setValue(int value) {
        this.value = value;
    }
}

Cet article de a joliment énuméré d'autres avantages de l'utilisation d'Enums: Instanciation java singleton


0 commentaires

3
votes

Vous ne comprenez pas vraiment ce que fait volatile , mais pour être juste, Internet et le stackoverflow, y compris, sont simplement pollués par des réponses erronées ou incomplètes à ce sujet. J'avoue aussi que je pense que j'ai une bonne idée de ce sujet, mais que je dois parfois relire certaines choses.

Ce que vous avez montré ici - s'appelle l'idiome "double contrôle de verrouillage" et c'est un cas d'utilisation parfaitement valide pour créer un singleton. La question est de savoir si vous en avez vraiment besoin dans votre cas (l'autre réponse a montré une manière beaucoup plus simple, ou vous pouvez également lire le "enum singleton pattern" si vous le souhaitez). C'est un peu drôle de voir combien de personnes savent que volatile est nécessaire pour cet idiome, mais ne peuvent pas vraiment dire pourquoi cela est nécessaire.

DCL est faire principalement deux choses - assure l ' atomicité (plusieurs threads ne peuvent pas entrer dans le bloc synchronisé en même temps) et garantit qu'une fois créé, tous les threads verront cette instance créée, appelée visibilité . En même temps, cela garantit que le bloc synchronisé sera entré une seule fois , tous les threads suivants n'auront pas besoin de le faire.

Vous auriez pu le faire facilement via :

 public Singleton get() {
    if (instance == null) {
        synchronized (this) {
            if (instance == null) {
                Singleton copy = new Singleton();
                copy.setSome(new Object());
                instance = copy;
            }
        }
    }
    return instance; 
}

Mais maintenant, chaque thread qui a besoin de cette instance doit entrer en compétition pour le verrou et doit entrer dans ce bloc synchronisé.

Certaines personnes pensent que: "hé, je peux contourner ça!" et écrivez (entrez donc le bloc synchronisé seulement une fois):

private Singleton instance;

public Singleton get() {
    if (instance == null) {  
        synchronized (this) {
            if (instance == null) { 
                instance = new Singleton();
                instance.setSome(new Object());
            }
        }
    }
    return instance; 
}

Aussi simple que cela soit - c'est cassé . Et ce n'est pas facile à expliquer.

  • il est cassé car il y a deux lectures indépendantes de instance ; JMM permet de les commander à nouveau; il est donc entièrement valide que if (instance == null) ne voit pas de null; tandis que return instance; voit et renvoie un null . Oui, c'est contre-intuitif, mais tout à fait valide et prouvable (je peux écrire un test jcstress pour le prouver en 15 minutes).

  • le deuxième point est un peu plus délicat. Supposons que votre singleton ait un champ que vous devez définir.

Regardez cet exemple:

static class Singleton {

    private Object some;

    public Object getSome() {
        return some;
    }

    public void setSome(Object some) {
        this.some = some;
    }
}

Et vous écrivez un code comme celui-ci pour fournir ce singleton:

  private Singleton instance; // no volatile

  public Singleton get() {
    if (instance == null) {  
      synchronized (this) {
        if (instance == null) { 
          instance = new Singleton();
        }
      }
    }
    return instance; 
  }

Puisque l ' écriture dans le volatile ( instance = new Singleton (); ) se produit avant le réglage champ dont vous avez besoin instance.setSome (new Object ()); ; un thread qui lit cette instance peut voir que instance n'est pas null, mais lors de l'exécution de instance.getSome () , il verra un null. La bonne façon de procéder serait (en plus de rendre l'instance volatile):

  private Singleton instance;

  public Singleton get() {
    synchronized (this) {
      if (instance == null) {
        instance = new Singleton();
      }
      return instance;
    }
  }

Ainsi volatile ici est nécessaire pour publication sécurisée ; afin que la référence publiée soit "en toute sécurité" vue par tous les threads - tous ses champs sont initialisés. Il existe d'autres moyens de publier en toute sécurité une référence, comme final défini dans le constructeur, etc.

Fait de la vie: les lectures sont moins chères que les écritures ; vous ne devriez pas vous soucier de ce que les lectures volatiles font sous le capot tant que votre code est correct; alors ne vous inquiétez pas des "lectures depuis la mémoire principale" (ou mieux encore, n'utilisez pas cette phrase sans même la comprendre partiellement).


7 commentaires

Vous ne comprenez pas en quoi consiste volatile , bien que votre explication fonctionne toujours assez bien. Ce que fait précisément volatile , et qui se reflète dans l'explication de «visibilité», c'est qu'il récupérera toujours la valeur de la variable de la mémoire principale, même si elle est déjà dans le cache du processeur. Les magasins sont de toute façon vidés dans la mémoire principale; volatile a peu d'influence sur cela. Cela a un effet sur ce que le compilateur peut faire en ce qui concerne la réorganisation. Cependant, volatile entraîne une surcharge de performances significative pour la lecture dans les applications multithreads.


Vous n'avez pas à proprement parler besoin de volatile OU synchronized si vous pouvez en quelque sorte vous assurer que la première écriture créant l'instance Singleton se produit avant que tout autre thread n'essaie de lire la variable d'instance. Tout autre thread essaiera de récupérer la valeur du cache, échouera et la récupérera quand même de la mémoire principale. Là où cela est très dangereux, c'est lorsque vous ne pouvez pas garantir cela, ou lorsque vous avez affaire à une variable qui est beaucoup modifiée, c'est-à-dire pas un Singleton.


Ne vous inquiétez pas trop des membres Object ( getSome () / setSome () et des certains sous-jacents), cependant. La variable d'instance contient uniquement une référence à l'objet Singleton en mémoire. Autrement dit, tant que le Singleton reste le même objet, vous référencerez toujours la même mémoire, ce qui rend l'utilisation du cache du processeur sûre. Cependant, si setSome () est beaucoup appelé, je rendrais très certainement le membre some volatil , pour les raisons exposées ci-dessus.


TL; DR est, en cas de doute, toujours utiliser volatile pour s'assurer que les valeurs sont toujours les mêmes dans les threads. Il y a un impact distinct sur les performances, mais cela n'a d'importance que si vous accédez à la valeur beaucoup et que vous vous souciez à chaque fois de savoir si c'est la plus récente.


synchronized d'autre part garantit que la ligne new Singleton () n'est exécutée qu'une seule fois. C'est vital lorsque la création du Singleton est coûteuse, ou lorsque vous ne pouvez pas vous assurer que les threads de lecture se produisent après la création de l'instance (voir premier commentaire). En cas de doute, utilisez-le. Et si vous utilisez les deux, DCL s'assurera que le bloc synchronized ne sera pas entré inutilement, ce qui améliore les performances.


Je m'attends à ce que @Eugene discute un peu, mais je ne vais pas répondre - il y a trop peu de lumière dans la journée à consacrer à cette conversation.


@JensFinkhaeuser merci pour les commentaires, dommage que vous ne vouliez pas en discuter plus, cependant ...