2009-10-25 6 views
12

concurrency Lecture Java dans la pratique, la section 3.5: la revendication est soulevée quepas thread-safe publication objet

public Holder holder; 
public void initialize() { 
    holder = new Holder(42); 
} 

Outre le fil évidente en toute sécurité de risque de créer 2 cas de titulaire le livre prétend un problème d'édition possible peut se produire, plus encore pour une classe Holder telle que

public Holder { 
    int n; 
    public Holder(int n) { this.n = n }; 
    public void assertSanity() { 
     if(n != n) 
      throw new AssertionError("This statement is false."); 
    } 
} 

une erreur AssertionError peut être levée!

Comment est-ce possible? La seule chose à laquelle je peux penser qui peut permettre un tel comportement ridicule est que le constructeur Holder ne bloque pas, donc une référence serait créée pour l'instance alors que le code du constructeur s'exécute toujours dans un thread différent. Est-ce possible ?

+0

trouvé un lien en ligne à la section du livre http://book.javanb.com/java-concurrency-in-Practice/ch03lev1sec5.html –

+0

Est-ce que cela veut dire tout le champ dans un objet doit être définitif. Y at-il de toute façon je peux me prouver que cela peut arriver. J'ai essayé s'il vous plaît aider – John

Répondre

13

La raison pour laquelle cela est possible est que Java a un modèle de mémoire faible. Cela ne garantit pas la commande de lecture/écriture.Ce problème particulier peut repro avec les 2 extraits de code suivants représentant 2 fils

Discussion 1:

someStaticVariable = new Holder(42); 

Discussion 2:

someStaticVariable.assertSanity(); // can throw 

Sur la surface, il semble impossible que cela puisse jamais se produire. Afin de comprendre pourquoi cela peut arriver, vous devez dépasser la syntaxe Java et descendre à un niveau beaucoup plus bas. Si vous regardez le code pour le fil 1, il peut essentiellement être décomposé en une série de mémoire écrit et les allocations

  1. Alloc mémoire à pointer1
  2. Ecrire 42 à pointer1 à l'offset 0
  3. Ecrire pointer1 à someStaticVariable

Parce que Java a un modèle de mémoire faible, il est parfaitement possible que le code s'exécute dans l'ordre suivant du point de vue thread2.

  1. Alloc mémoire à pointer1
  2. Ecrire pointer1 à someStaticVariable
  3. Ecrire à 42 pointer1 à l'offset 0

Effrayant? Oui mais ça peut arriver Cela signifie cependant que Thread2 peut maintenant appeler assertSanity avant que n ne reçoive la valeur 42. Il est possible que la valeur n soit lue deux fois pendant assertSanity, une fois avant que l'opération # 3 se termine et une fois après et donc voir 2 valeurs différentes et lancer une exception.

EDIT

Selon Jon ce n'est pas possible (heureusement) avec les nouvelles versions de Java en raison de mises à jour du modèle de mémoire.

EDIT 2

Selon Jon, il ne dit jamais qu'il est impossible avec la version 8 de Java à moins que le champ est définitif.

+0

@Jared: J'avais oublié que c'est seulement garanti pour les champs finaux. IIRC, la spécification ECMA CLI est également faible à cet égard, mais le modèle de mémoire .NET rend toutes les écritures réellement volatiles. C'est seulement l'IIRC si :) –

+0

@Jon vous avez raison sur les angles .Net et ECMA. Cela rend le jeu intéressant en examinant les changements apportés aux librairies F # et à la base de code, car elles sont conformes au modèle ECMA. – JaredPar

+0

Effrayant en effet.J'ai appris de vous répondre quelque chose que je n'avais aucune idée concernant le modèle de mémoire java - Cela me fait peur car il peut effectivement signifier 90% de tous les codes Java dans le monde est cassé. De plus, la rapidité de votre réponse m'étonne. De mon calcul il vous a fallu un total de ~ 5min pour répondre à ma question! Merci beaucoup pour l'effort. –

9

Le modèle de mémoire Java a utilisé pour que l'affectation à la référence Holder devienne visible avant l'affectation à la variable dans l'objet. Cependant, le modèle de mémoire plus récent qui a pris effet à Java 5 rend cela impossible, au moins pour les champs finaux: toutes les affectations au sein d'un constructeur «arrivent avant» toute assignation de la référence au nouvel objet à une variable. Voir la Java Language Specification section 17.4 pour plus de détails, mais voici l'extrait le plus pertinent:

Un objet est considéré comme initialisés complètement lorsque ses finitions constructeur . Un fil qui ne peut voir une référence à un objet après cet objet a été complètement initialisé est garanti pour voir les valeurs initialisés correctement pour les champs finaux de cet objet

Ainsi, votre exemple pourrait encore échouer comme n est non-finale, mais cela devrait être correct si vous faites n final.

Bien sûr, le:

if (n != n) 

pourrait certainement échouer pour les variables non-finales, en supposant que le compilateur JIT n'optimise pas loin - si les opérations sont les suivantes:

  • Fetch LHS: n
  • Fetch RHS: n
  • Comparez LHS et RHS

alors la valeur pourrait changer entre les deux extractions.

+0

Est-ce que cela signifie que tout le champ dans un objet doit être définitif. Y at-il de toute façon je peux me prouver que cela peut arriver. J'ai essayé s'il vous plaît aider – John

+0

javap montre qu'il ne l'optimiser pour que votre distance hypothèse est correcte: 1: getfield # 2 4: aload_0 5: getfield # 2 8: if_icmpeq 21 –

+0

Ce que vous citez est sur la sémantique finale. Mais pour cette déclaration: Titulaire titulaire = nouveau Titulaire(), dernière JMM peut garantir est exécuté avant astore? pourriez-vous me dire la section dans JLS. – Chao

-1

regardant ce dans une perspective saine d'esprit, si vous supposez que la déclaration

if(n != n)

est atomique (que je pense est raisonnable, mais je ne sais pas pour sûr), à l'exception de l'affirmation ne pourrait jamais être jeté.

+0

Quelqu'un veut-il expliquer le -1? – twolfe18

+0

n! = N n'est pas atomique, il lit deux fois les valeurs de n. –

0

Le problème de base est que, sans une synchronisation correcte, comment les écritures en mémoire peuvent se manifester dans différents threads. L'exemple classique:

a = 1; 
b = 2; 

Si vous le faites sur un fil, un deuxième fil peut voir b ensemble à 2 avant un est fixé à 1. De plus, il est possible qu'il y ait une quantité illimitée de temps entre un deuxième thread en voyant une de ces variables se mettre à jour et l'autre variable en cours de mise à jour.

1

Eh bien, dans le livre, il déclare pour le premier bloc de code:

Le problème ici n'est pas la classe Holder lui-même, mais que le titulaire est pas correctement publié. Cependant, Holder peut être à l'abri de mauvaise publication en déclarant le champ n être définitif, ce qui rendrait Holder immuable; voir la section 3.5.2

Et pour le second bloc de code:

Parce que la synchronisation n'a pas été utilisé pour rendre le support visible pour les autres fils, nous disons que le titulaire n'a pas été correctement publié. Deux choses peuvent aller mal avec des objets mal publiés. D'autres discussions pourraient voir une valeur rassis pour le champ de support, et voir ainsi une référence nulle ou autre valeur plus même si une valeur a été placé dans le support. Mais bien pire, autres threads pourraient voir une mise todate valeur pour la référence du support, mais valeurs périmées pour l'état du titulaire . [16] Pour rendre les choses encore moins prévisibles , un fil peut voir une valeur obsolète la première fois qu'il lit un champ puis un plus à jour valeur la prochaine fois, ce qui explique pourquoi assertSanity peut jeter AssertionError.

Je pense que JaredPar a rendu cela explicite dans son commentaire.

(Note: Vous ne cherchez pas ici votes - réponses permettent d'informations plus détaillées que les commentaires.)

-1

Cet exemple vient sous la rubrique « Une référence à l'objet contenant le champ final n'a pas échappé au constructeur »

Lorsque vous instancier un nouvel objet Titulaire avec le nouvel opérateur,

  1. la machine virtuelle Java d'abord allouera (au moins) assez d'espace sur le tas pour contenir toutes les variables d'instance déclarées dans Holder et ses superclasses.
  2. Deuxièmement, la machine virtuelle initialiser toutes les variables d'instance à leurs valeurs initiales par défaut. 3.c Troisièmement, la machine virtuelle appelle la méthode dans la classe Holder.

s'il vous plaît se référer pour ci-dessus: http://www.artima.com/designtechniques/initializationP.html

On suppose: 1er fil commence 10:00, il appelle instatied l'objet Titulaire en faisant l'appel de nouveaux Holer (42), 1) la machine virtuelle Java d'abord allouer (au moins) suffisamment d'espace sur le tas pour contenir toutes les variables d'instance déclarées dans Holder et ses superclasses. - il 10:01 temps 2) En second lieu, la machine virtuelle initialiser toutes les variables d'instance à leurs valeurs par défaut initiales - il commencera à 10h02 heure 3) En troisième lieu, la machine virtuelle invoquera la méthode dans la Classe de détenteur.- il commencera 10:04 heure

Maintenant Thread2 a commencé à -> 10:02:01 heure, et il fera un appel assertSanity() 10:03, à ce moment n a été initialisé avec zéro par défaut , Deuxième thread lire les données périmées.

// publication non sécuritaire porte Holder public;

si vous faites le porte-support public final résoudra ce problème

ou

n private int; si vous faites la finale privée n n; résoudra ce problème.

s'il vous plaît consulter: http://www.cs.umd.edu/~pugh/java/memoryModel/jsr-133-faq.html en section Comment les champs fonctionnent conjointement dans le cadre du nouveau JMM?

-1

J'étais aussi très intrigué par cet exemple. J'ai trouvé un site Web qui explique le sujet à fond et les lecteurs pourraient trouver utiles: https://www.securecoding.cert.org/confluence/display/java/TSM03-J.+Do+not+publish+partially+initialized+objects

Edit: Le texte pertinent du lien dit:

la JMM permet compilateurs d'allouer de la mémoire pour le nouvel assistant objet et pour attribuer une référence à cette mémoire pour le champ auxiliaire avant d'initialiser le nouvel objet d'assistance. En d'autres termes, le compilateur peut réorganiser l'écriture dans le champ d'instance auxiliaire et la écriture qui initialise l'objet utilitaire (qui est, this.n = n) de telle sorte que le premier se produit en premier. Cela peut exposer une fenêtre de course au cours de laquelle autres threads peuvent observer un objet Helper partiellement initialisé instance.

Questions connexes