42 votes

L'immuabilité et la réorganisation des

Commentaire sur accepté de répondre à

Cette question a généré beaucoup plus de chaleur que je woul dhave imaginé. Une importante conclusion que je tire de public et de privé discussions avec les membres de la simultanéité de l'intérêt liste de diffusion (c'est à dire les gens qui travaillent effectivement sur la mise en œuvre de ces choses):

Si vous pouvez trouver un séquentiellement cohérente réorganisation qui ne casse pas tout inter-thread-passe-avant, il est valide de réorganisation (c'est à dire compatible avec le programme de la règle de l'ordre et le lien de causalité requis).

C'est ce que John Vint a fournis dans sa réponse.


Question d'origine

Le code ci-dessous (Java Simultanéité dans la Pratique liste 16.3) n'est pas thread-safe pour des raisons évidentes:

public class UnsafeLazyInitialization {
    private static Resource resource;

    public static Resource getInstance() {
        if (resource == null)
            resource = new Resource();  // unsafe publication
        return resource;
    }
}

Cependant, quelques pages plus loin, dans la section 16.3, ils affirment:

UnsafeLazyInitialization est réellement sans danger si Resource est immuable.

Je ne comprends pas cette déclaration:

  • si Resource est immuable, de n'importe quel thread observation de l' resource variable est nulle ou entièrement construite (grâce à la forte garanties de finale champs fournis par le Modèle Mémoire Java)
  • toutefois, rien n'empêche d'instruction de la réorganisation: en particulier, les deux lectures de l' resource pourraient être réorganisées (il y en a un de lire dans l' if et un dans l' return). Si un thread pourrait voir un non null resource dans la if , dans l'état de retour d'une référence null (*).

Je pense que UnsafeLazyInitialization.getInstance() peut retourner null même si Resource est immuable. Est-ce le cas et pourquoi (ou pourquoi pas)?

Note: je m'attends à un raisonnée des réponses plutôt que de la simple oui ou non consolidés.


(*) pour mieux comprendre mon point de vue à propos de la réorganisation, ce blog par Jeremy Manson, qui est l'un des auteurs du Chapitre 17 de la JLS sur la simultanéité, explique comment la Chaîne de hashcode en toute sécurité est publié par une tumeur bénigne de données de la race et de la façon dont la levée de l'utilisation d'une variable locale peut conduire à hashcode incorrectement retourne 0, en raison d'une possible réorganisation de très similaire à ce que je décris ci-dessus:

Ce que j'ai fait ici est d'ajouter une autre lecture: la lecture du deuxième de hachage, avant le retour. Aussi étrange que cela puisse paraître, et aussi invraisemblable que cela puisse se produire, à la première lecture peut retourner l'correctement calculé la valeur de hachage, et la deuxième lecture peut retourner 0! Ceci est permis en vertu du modèle de mémoire parce que le modèle permet une vaste réorganisation de ses activités. La deuxième lecture peut être déplacée, dans votre code, de sorte que votre processeur ne elle avant de la première!

3voto

GaborSch Points 6587

Mise à JOUR Feb10

Je suis convaincu que nous devons nous séparer en 2 phases: la compilation et l'exécution.

Je pense que le facteur de décision de savoir s'il est autorisé à retourner la valeur null ou n'est ce que le bytecode est. J'ai fait 3 exemples:

Exemple 1:

Le code source d'origine, traduit littéralement par "bytecode":

if (resource == null)
    resource = new Resource();  // unsafe publication
return resource;

Le pseudo-code:

public static Resource getInstance();
Code:
0:   getstatic       #20; //Field resource:LResource;
3:   ifnonnull       16
6:   new             #22; //class Resource
9:   dup
10:  invokespecial   #24; //Method Resource."<init>":()V
13:  putstatic       #20; //Field resource:LResource;
16:  getstatic       #20; //Field resource:LResource;
19:  areturn

C'est le cas le plus intéressant, car il y a 2 reads (Ligne n ° 0 et n ° de la Ligne 16), et il est de 1 write inbetween (Ligne n ° 13). Je prétends que c'est pas possible de les réorganiser, mais nous allons examiner ci-dessous.

Exemple 2:

Le "compilateur optimisé" du code, qui peut être littéralement re-converti à java comme suit:

Resource read = resource;
if (resource==null)
    read = resource = new Resource();
return read;

Le byte code (en fait j'ai réalisé en compilant le code ci-dessus extrait):

public static Resource getInstance();
Code:
0:   getstatic       #20; //Field resource:LResource;
3:   astore_0
4:   getstatic       #20; //Field resource:LResource;
7:   ifnonnull       22
10:  new     #22; //class Resource
13:  dup
14:  invokespecial   #24; //Method Resource."<init>":()V
17:  dup
18:  putstatic       #20; //Field resource:LResource;
21:  astore_0
22:  aload_0
23:  areturn

Il est évident, que si le compilateur "optimise", et le byte code comme ci-dessus est produit, une valeur null de lecture peuvent se produire (par exemple, je me réfère à Jeremy Manson blog)

Il est également intéressant de voir que la façon dont a = b = c travaille: la référence à la nouvelle instance (Ligne n ° 14) est dupliqué (Ligne n ° 17), et la même référence est stockée puis, d'abord à l' b (ressource, (Ligne n ° 18)), puis à l' a (lire, (Ligne n ° 21)).

Exemple 3:

Nous allons faire encore plus légère modification: lire l' resource qu'une seule fois! Si le compilateur commence à optimiser (et l'utilisation de registres, comme d'autres l'ont mentionné), c'est mieux d'optimisation que ci-dessus, parce que la Ligne#4 ici est un "registre d'accès" plutôt que un plus cher "statique d'accès" dans l'Exemple 2.

Resource read = resource;
if (read == null)   // reading the local variable, not the static field
    read = resource = new Resource();
return read;

Le pseudo-code pour l'Exemple 3 (également créé avec, littéralement, la compilation ci-dessus):

public static Resource getInstance();
Code:
0:   getstatic       #20; //Field resource:LResource;
3:   astore_0
4:   aload_0
5:   ifnonnull       20
8:   new     #22; //class Resource
11:  dup
12:  invokespecial   #24; //Method Resource."<init>":()V
15:  dup
16:  putstatic       #20; //Field resource:LResource;
19:  astore_0
20:  aload_0
21:  areturn

Il est également facile de voir, qu'il n'est pas possible d'obtenir la valeur null à partir de ce bytecode, car il est construit de la même façon qu' String.hashcode(), le fait d'avoir seulement 1 lecture de la variable statique de l' resource.

Examinons maintenant l'Exemple 1:

0:   getstatic       #20; //Field resource:LResource;
3:   ifnonnull       16
6:   new             #22; //class Resource
9:   dup
10:  invokespecial   #24; //Method Resource."<init>":()V
13:  putstatic       #20; //Field resource:LResource;
16:  getstatic       #20; //Field resource:LResource;
19:  areturn

Vous pouvez voir que la Ligne n ° 16 (la lecture de variable#20 de retour), la plupart d'observer l'écriture de la Ligne#13 (l'assignation d' variable#20 du constructeur), de sorte qu'il est illégal de le placer à l'avance dans tout ordre d'exécution où la Ligne#13 est exécutée. Donc, pas de réorganisation est possible.

Pour une JVM, il est possible de construire (et de profiter des) branche (à l'aide de certaines conditions supplémentaires) contourne la Ligne#13 écrire: la condition est que la lecture de variable#20 ne doit pas être null.

Donc, dans les deux cas pour l'Exemple 1 est possible de retourner la valeur null.

Conclusion:

Voir les exemples ci-dessus, un bytecode vu dans l'Exemple 1 ne SERONT PAS PRODUIRE d' null. Une optimisation du bytecode comme dans l'Exemple 2 SERA PROCUDE null, mais il est encore une meilleure optimisation de l' Exemple 3, qui ne produiront PAS d' null.

Parce que nous ne pouvons pas être préparé à toute optimisation possible de tous les compilateurs, nous pouvons dire que , dans certains cas, il est possible, certains autres cas pas possible d' return null, et tout dépend du byte code. Aussi, nous avons montré qu' il existe au moins un exemple pour les deux cas.


Âgés de raisonnement: on parle par exemple de Assylias: La question principale est: est-il valable (concernant tous les specs, JMM, JLS) d'une machine virtuelle permettrait de réorganiser les 11 et 14 se lit donc, que 14 qui va arriver AVANT 11?

Si cela pouvait arriver, les indépendants, Thread2pourrait écrire la ressource avec 23, de 14 pourrait lire null. Je déclare qu'il n'est pas possible.

En fait, puisqu'il est possible d'écrire de 13 ans, il ne serait pas valide d'un ordre d'exécution. Une machine virtuelle peut optimiser l'exécution de l'ordre, donc, qui exclut les non-exécutée branches (reste juste 2 lit, non-écrit), mais pour prendre cette décision, il doit faire la première lecture (11), et il faut lire non nul, donc le 14 lire ne peut pas précéder le 11 lire. Donc, il n'est PAS possible de revenir null.


L'immutabilité

Concernant l'immuabilité, je pense que cette affirmation n'est pas vraie:

UnsafeLazyInitialization est réellement sans danger si la Ressource est immuable.

Toutefois, si le constructeur est imprévisible, les résultats les plus intéressants peut sortir. Imaginez un constructeur comme ceci:

public class Resource {
    public final double foo;

    public Resource() {
        this.foo = Math.random();
    }
}

Si nous avons tho Threads, il peut en résulter, que les 2 threads recevrez un autrement-se comporter de l'Objet. Ainsi, le texte complet de la déclaration devrait ressembler à ceci:

UnsafeLazyInitialization est réellement sans danger si la Ressource est immuable et son initialisation est cohérent.

Par cohérence , je veux dire que d'appeler le constructeur de la Resource deux fois, nous allons recevoir deux objets qui se comportent exactement de la même manière (en appelant les mêmes méthodes dans le même ordre sur les deux donneront les mêmes résultats).

3voto

John Vint Points 19804

La confusion, je pense que vous avez ici est ce que l'auteur entend par sécurité de publication. Il faisait allusion à la sécurité de la publication d'un non-nulle Ressource, mais vous semblez avoir que.

Votre question est intéressante, est-il possible de retourner une valeur nulle valeur mise en cache des ressources?

Oui.

Le compilateur est autorisé à réorganiser le fonctionnement comme tel

public static Resource getInstance(){
   Resource reordered = resource;
   if(resource != null){
       return reordered;
   }
   return (resource = new Resource());
} 

Ce n'enfreint pas la règle de cohérence séquentielle mais peut retourner une valeur null.

Que ce soit ou non la meilleure mise en œuvre est en débat, mais il n'y a pas de règles pour prévenir ce type de réorganisation.

3voto

assylias Points 102015

Après l'application de la JLS règles pour cet exemple, j'en suis venu à la conclusion qu' getInstance peut certainement revenir null. En particulier, JLS 17.4:

Le modèle de mémoire détermine quelles valeurs peut être lu à chaque point du programme. Les actions de chaque thread dans l'isolement doit se comporter en tant que régie par la sémantique de ce thread, à l'exception que les valeurs pour chaque lecture sont déterminés par le modèle de mémoire.

Il est alors clair que , en l'absence de synchronisation, null est un résultat de la méthode puisque chacune des deux lectures peuvent observer quoi que ce soit.


La preuve

La décomposition de lectures et d'écritures

Le programme peut être décomposé comme suit (à voir clairement le lit et écrit):

                              Some Thread
---------------------------------------------------------------------
 10: resource = null; //default value                                  //write
=====================================================================
           Thread 1               |          Thread 2                
----------------------------------+----------------------------------
 11: a = resource;                | 21: x = resource;                  //read
 12: if (a == null)               | 22: if (x == null)               
 13:   resource = new Resource(); | 23:   resource = new Resource();   //write
 14: b = resource;                | 24: y = resource;                  //read
 15: return b;                    | 25: return y;                    

Ce que l'JLS dit

JLS 17.4.5 donne les règles pour une lecture à permis d'observer une réduction de:

Nous disons que lire une r d'une variable v est permis d'observer une réduction de w de v si, dans le passe-avant partielle de l'ordre de la trace d'exécution:

  • r n'est pas commandé avant w (c'est à dire, il n'est pas le cas que le hb(r, w)), et
  • il est sans période d'écrire w' v (c'est à dire pas écrire w de v tel que hb(w, w') et hb(w, r)).

L'Application de la règle

Dans notre exemple, supposons que le thread 1 voit nulle et correctement initialise resource. Dans le thread 2, une défaillance de l'exécution serait de 21 à 23 observer (en raison du programme de commande) - mais que toutes les autres écritures (10 et 13) peut être observé par de lecture:

  • 10-passe-avant toutes les actions, donc pas de lecture est ordonnée avant le 10
  • 21 et 24 ont pas de hb relation avec le 13
  • 13 n'est-passe-avant 23 (hb relation entre les deux)

Donc, à la fois, les 21 et 24 (2 lectures) sont autorisés à observer, soit 10 (null) ou 13 (not null).

Chemin d'exécution qui retourne null

En particulier, en supposant que le Thread 1 voit un nul sur la ligne 11 et initialise resource sur la ligne 13, le Thread 2 pourrait exécuter légalement comme suit:

  • 24: y = null (lit écrire 10)
  • 21: x = non null (lit d'écriture 13)
  • 22: false
  • 25: return y

Remarque: pour préciser, cela ne signifie pas que T2 voit non nulle et par la suite voit nulle (ce qui serait en violation de la causalité requise) - cela signifie que, d'un point de vue de l'exécution, les deux lectures ont été réorganisées et le deuxième a été commis avant le premier - mais il n'a pas l'air comme si le plus tard écrire a été vu, avant que la précédente sur la base de la commande du programme.

Mise à JOUR 10 Février

Retour à du code, valide la réorganisation serait:

Resource tmp = resource; // null here
if (resource != null) { // resource not null here
    resource = tmp = new Resource();
}
return tmp; // returns null

Et parce que le code est séquentiellement compatible (si elle est exécutée par un seul thread, il aura toujours le même comportement que le code d'origine), il montre que la causalité exigences sont satisfaites (il y a une validité qui produit le résultat).


Après publication sur la simultanéité liste d'intérêt, j'ai eu quelques messages concernant la légalité de cette réorganisation, qui confirment que l' null est un résultat juridique:

  • La transformation est certainement juridique dans la mesure où un seul thread d'exécution de ne pas faire la différence. [Note] que la transformation ne semble pas raisonnable, il n'y a pas de bonne raison pour un compilateur pourrait le faire. Toutefois, étant donné une plus grande quantité de code environnant ou peut-être une optimisation du compilateur "bug", il pourrait arriver.
  • La déclaration sur le commerce intra-fil de commande et de commande du programme est ce qui m'a fait remettre en question la validité de choses, mais en fin de compte le JMM concerne le pseudo-code qui est exécuté. La transformation peut être effectuée par le compilateur javac, auquel cas la valeur null sera parfaitement valable. Et il n'y a pas de règles pour savoir comment javac a pour convertir de la source Java bytecode Java, donc...

2voto

Markus A. Points 3969

Il y a essentiellement deux questions que vous lui posez:

1. Peut l' getInstance() de retour de méthode null en raison de la réorganisation?

(je pense que c'est ce que vous êtes vraiment après, donc je vais essayer d'y répondre en premier)

Même si je pense que la conception de Java pour permettre pour ce qui est totalement fou, il semble que vous êtes tout à fait correct qu' getInstance() peut retourner la valeur null.

Votre exemple de code:

if (resource == null)
    resource = new Resource();  // unsafe publication
return resource;

est logiquement 100% identique à l'exemple dans le billet de blog est lié à:

if (hash == 0) {
    // calculate local variable h to be non-zero
    hash = h;
}
return hash;

Jeremy Manson, puis il décrit que son code peut retourner 0 en raison de la réorganisation. Au début, je ne l'ai pas cru que je pensais que le suivant "passe-avant"-logique doit tenir:

   "if (resource == null)" happens before "resource = new Resource();"
                                   and
     "resource = new Resource();" happens before "return resource;"
                                therefore
"if (resource == null)" happens before "return resource;", preventing null

Mais Jeremy donne l'exemple suivant dans un commentaire sur son blog, comment ce code peut être valablement réécrit par le compilateur:

read = resource;
if (resource==null)
    read = resource = new Resource();
return read;

Ce, dans un seul thread de l'environnement, se comporte exactement identique à l'original du code, mais, dans un environnement multi-thread peut conduire à la suite de l'exécution de la commande:

Thread 1                        Thread 2
------------------------------- -------------------------------------------------
read = resource;    // null
                                read = resource;                      // null
                                if (resource==null)                   // true
                                    read = resource = new Resource(); // non-null
                                return read;                          // non-null
if (resource==null) // FALSE!!!
return read;        // NULL!!!

Maintenant, à partir d'une optimisation du point de vue, cela ne fait aucun sens pour moi, depuis le point de l'ensemble de ces choses serait de réduire les multiples lectures au même endroit, auquel cas il ne fait aucun sens que le compilateur ne génère pas d' if (read==null) au lieu de cela, à la prévention du problème. Donc, comme Jeremy points dans son blog, il est probablement très peu probable que cela arrive un jour. Mais il semble que, purement à partir d'une langue-règles de point de vue, il est en fait autorisé.

Cet exemple est en fait couverte dans le JLS:

http://docs.oracle.com/javase/specs/jls/se7/html/jls-17.html#jls-17.4

L'effet observé entre les valeurs de r2, r4, et r5 en Table 17.4. Surprising results caused by forward substitution est équivalent à ce qui peut se produire avec l' read = resource, l' if (resource==null), et l' return resource dans l'exemple ci-dessus.

Aparté: Pourquoi dois-je faire référence le blog en tant que source ultime pour la réponse? Parce que le mec qui l'a écrit, est aussi le gars qui a écrit le chapitre 17 de l'JLS sur la simultanéité! Donc, il vaut mieux être de droite! :)

2. Seraient Resource immuable faire l' getInstance() méthode thread-safe?

Étant donné le potentiel null de résultat, ce qui peut se produire indépendamment du fait que d' Resource est mutable ou pas, la réponse simple à cette question est: Non (non strictement)

Si nous ignorons ce très peu probable, mais possible scénario, cependant, la réponse est: Dépend.

L'évidence thread problème avec le code, c'est qu'il peut conduire à la suite de l'exécution de l'ordre (sans le besoin pour n'importe quel réorganisation):

Thread 1                                 Thread 2
---------------------------------------- ----------------------------------------
if (resource==null) // true;  
                                         if (resource==null)          // true
                                             resource=new Resource(); // object 1
                                         return resource;             // object 1
    resource=new Resource(); // object 2
return resource;             // object 2

Ainsi, la non-thread-safety est venue du fait que vous pourriez obtenir deux objets différents de retour de la fonction (même si sans réorganisation aucun d'eux ne sera jamais null).

Maintenant, ce que le livre était probablement en train de dire est la suivante:

La Java des objets immuables comme les Chaînes et les nombres Entiers essayez d'éviter de créer plusieurs objets pour le même contenu. Donc, si vous avez "hello" en un seul endroit et "hello" dans un autre endroit, Java vous donnera exactement la même référence d'objet. De même, si vous avez new Integer(5) en un seul endroit et new Integer(5) dans un autre. Si c'était le cas avec new Resource() ainsi, vous obtenez les mêmes références et object 1 et object 2 dans l'exemple ci-dessus serait exactement le même objet. Cela conduirait en effet à un "thread-safe" de la fonction (en ignorant la réorganisation de problème).

Mais, si vous implémentez Resource - toi, je ne crois pas qu'il existe même une façon pour le constructeur de renvoyer une référence à un objet créé précédemment plutôt que d'en créer un nouveau. Donc, il ne devrait pas être possible pour vous de faire object 1 et object 2 être exactement le même objet. Mais, étant donné que vous êtes d'appeler le constructeur avec les mêmes arguments (aucun dans les deux cas), il pourrait être probable que, même si vos objets créés ne sont pas exactement le même objet, ils seront, à toutes fins et intentions, se comportent comme si elles l'étaient, aussi efficacement du code thread-safe.

Cela ne doit pas nécessairement être le cas. Imaginez un immuable version de Date, par exemple. Le constructeur par défaut Date() utilise le système actuel, le temps que la date de valeur. Donc, même si l'objet est immuable et le constructeur est appelé avec le même argument, l'appelant deux fois ne sera probablement pas le résultat dans un objet équivalent. Par conséquent, l' getInstance() méthode n'est pas thread-safe.

Donc, comme une déclaration générale, je crois que la ligne que vous avez cité le livre est tout simplement faux (à moins que sorti de son contexte ici).

OUTRE Re: réorganisation

Je trouve l' resource==new Resource() exemple un peu trop simpliste pour m'aider à comprendre POURQUOI le fait de permettre une telle réorganisation par Java ferais jamais de sens. Alors permettez-moi de voir si je peux venir avec quelque chose, si cela peut effectivement aider à l'optimisation:

System.out.println("Found contact:");
System.out.println(firstname + " " + lastname);
if (firstname==null) firstname = "";
if (lastname ==null) lastname  = "";
return firstname + " " + lastname;

Ici, dans la plupart des cas probable que les deux ifs rendement false, il n'est pas optimale à faire cher la concaténation de Chaîne firstname + " " + lastname deux fois, une fois pour le message de débogage, une fois pour le retour. Donc, il serait en effet de bon sens ici pour réorganiser le code pour faire ce qui suit:

System.out.println("Found contact:");
String contact = firstname + " " + lastname;
System.out.println(contact);
if ((firstname==null) || (lastname==null)) {
    if (firstname==null) firstname = "";
    if (lastname ==null) lastname  = "";
    contact = firstname + " " + lastname;
}
return contact;

À titre d'exemples deviennent plus complexes et que vous commencez à penser à le compilateur de garder la trace de ce qui est déjà chargé/calculé dans le processeur registres qu'il utilise intelligemment et sauter de re-calcul des résultats, cet effet pourrait en fait devenir plus et plus susceptibles de se produire. Donc, même si je n'ai jamais pensé que je pourrais jamais dire que quand je suis au lit la nuit dernière, en y réfléchissant plus, je ne fait maintenant croire que cela peut avoir été un besoin/une bonne décision pour vraiment permettre l'optimisation du code pour faire sa partie la plus impressionnante de la magie. Mais il ne me paraît assez dangereux car je ne pense pas que beaucoup de gens sont conscients de cela et même s'ils le sont, c'est assez complexe pour envelopper votre tête autour de la façon d'écrire votre code correctement sans la synchronisation de tout (qui sera alors de faire disparaître plusieurs fois avec toute la performance, avantages plus flexible de l'optimisation).

Je suppose que si vous n'avez pas permettre cette réorganisation, toute la mise en cache et la réutilisation des résultats intermédiaires d'une série d'étapes de processus deviendrait illégal, s'éloignant l'un des plus puissants les optimisations du compilateur possible.

0voto

Sean Owen Points 36577

Rien ne définit la référence à l' null une fois qu'il est non-null. Il est possible pour un thread pour voir null après un autre thread a mis à la non-null mais je ne vois pas comment l'inverse est possible.

Je ne suis pas sûr de l'instruction de re-commande est un facteur, mais l'entrelacement des instructions par deux threads est. L' if de la branche ne peuvent en quelque sorte être réorganisées à exécuter avant que son état a été évalué.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X