Domanda

Lettura " Concorrenza Java in pratica " ;, c'è questa parte nella sezione 3.5:

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

Oltre all'ovvio pericolo per la sicurezza del thread di creare due istanze di Holder , il libro afferma che può verificarsi un possibile problema di pubblicazione.

Inoltre, per una classe Holder come

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

un AssertionError può essere lanciato!

Come è possibile? L'unico modo in cui riesco a pensare che può consentire un comportamento così ridicolo è se il costruttore Holder non bloccherebbe, quindi verrà creato un riferimento all'istanza mentre il codice del costruttore viene ancora eseguito in un thread diverso.

È possibile?

È stato utile?

Soluzione

Il motivo per cui questo è possibile è che Java ha un modello di memoria debole. Non garantisce l'ordinamento di lettura e scrittura.

Questo particolare problema può essere riprodotto con i seguenti due frammenti di codice che rappresentano due thread.

Discussione 1:

someStaticVariable = new Holder(42);

Discussione 2:

someStaticVariable.assertSanity(); // can throw

In superficie sembra impossibile che ciò possa mai accadere. Per capire perché questo può accadere, devi superare la sintassi Java e scendere a un livello molto più basso. Se guardi il codice per il thread 1, può essenzialmente essere suddiviso in una serie di scritture e allocazioni di memoria:

  1. Alloca memoria al pointer1
  2. Scrivi 42 nel puntatore1 all'offset 0
  3. Scrivi pointer1 in someStaticVariable

Poiché Java ha un modello di memoria debole, è perfettamente possibile che il codice venga effettivamente eseguito nel seguente ordine dalla prospettiva del thread 2:

  1. Alloca memoria al pointer1
  2. Scrivi pointer1 in someStaticVariable
  3. Scrivi 42 nel puntatore1 all'offset 0

Spaventoso? Sì, ma può succedere.

Ciò significa che il thread 2 ora può chiamare assertSanity prima che n abbia ottenuto il valore 42. È possibile per il valore n da leggere due volte durante assertSanity , una volta prima che l'operazione n. 3 venga completata e una volta dopo, quindi vedere due valori diversi e generare un'eccezione.

Modifica

Secondo Jon Skeet , il AssertionError potrebbe ancora verificarsi con Java 8 a meno che il campo non sia definitivo.

Altri suggerimenti

Il modello di memoria Java utilizzato è tale da rendere visibile l'assegnazione al riferimento Holder prima dell'assegnazione alla variabile all'interno dell'oggetto.

Tuttavia, il modello di memoria più recente che è entrato in vigore a partire da Java 5 rende questo impossibile, almeno per i campi finali: tutte le assegnazioni all'interno di un costruttore "avvengono prima". qualsiasi assegnazione del riferimento al nuovo oggetto a una variabile. Vedi la sezione 17.4 delle specifiche del linguaggio Java per maggiori dettagli, ma ecco lo snippet più pertinente:

  

Un oggetto è considerato come   completamente inizializzato quando è   finiture del costruttore. Un filo che   può vedere solo un riferimento a un oggetto   dopo che l'oggetto è stato completamente   inizializzato è garantito per vedere il file   valori correttamente inizializzati per questo   campi finali dell'oggetto

Quindi il tuo esempio potrebbe ancora fallire poiché n non è definitivo, ma dovrebbe andare bene se rendi n definitivo.

Ovviamente il:

if (n != n)

potrebbe certamente fallire per le variabili non finali, supponendo che il compilatore JIT non lo ottimizzi da solo - se le operazioni sono:

  • Scarica LHS: n
  • Scarica RHS: n
  • Confronta LHS e RHS

quindi il valore potrebbe cambiare tra i due recuperi.

Bene, nel libro si afferma per il primo blocco di codice che:

  

Il problema qui non è il Titolare   classe stessa, ma che il Titolare è   non pubblicato correttamente. Però,   Il titolare può essere reso immune da improprio   pubblicazione dichiarando il campo n   essere definitivo, il che renderebbe Holder   immutabile; vedere la sezione 3.5.2

E per il secondo blocco di codice:

  

Perché la sincronizzazione non è stata utilizzata   per rendere il Titolare visibile agli altri   discussioni, diciamo che il titolare non lo era   pubblicato correttamente. Due cose possono andare   sbagliato con pubblicazione impropria   oggetti. Altre discussioni potrebbero vedere a   valore non aggiornato per il campo del supporto e   quindi vedere un riferimento null o altro   valore precedente anche se ha un valore   stato inserito nel supporto. Ma molto peggio,   altri thread potrebbero vedere un aggiornamento   valore per il riferimento del titolare, ma   valori non aggiornati per lo stato di   Holder. [16] Per rendere le cose ancora meno   prevedibile, un thread può vedere una raffermo   valore la prima volta che legge un campo   e quindi un valore più aggiornato il   la prossima volta, ecco perché assertSanity   può lanciare AssertionError.

Penso che JaredPar abbia reso questo esplicito nel suo commento.

(Nota: qui non cerchi voti: le risposte consentono informazioni più dettagliate dei commenti.)

Il problema di base è che senza una corretta sincronizzazione, il modo in cui le scritture in memoria possono manifestarsi in thread diversi. L'esempio classico:

a = 1;
b = 2;

Se lo fai su un thread, un secondo thread potrebbe vedere b impostato su 2 prima che a sia impostato su 1. Inoltre, è possibile che ci sia una quantità illimitata di tempo tra un secondo thread e che venga visualizzata una di quelle variabili aggiornato e l'altra variabile in fase di aggiornamento.

guardando questo da una prospettiva sana, se supponi che l'affermazione

if (n! = n)

è atomico (che penso sia ragionevole, ma non lo so per certo), quindi l'eccezione di asserzione non potrebbe mai essere lanciata.

Questo esempio è riportato in " Un riferimento all'oggetto contenente il campo finale non è sfuggito al costruttore "

Quando si crea un'istanza di un nuovo oggetto Holder con il nuovo operatore,

  1. la macchina virtuale Java per prima cosa alloca (almeno) abbastanza spazio sull'heap per contenere tutte le variabili di istanza dichiarate in Holder e le sue superclassi.
  2. In secondo luogo, la macchina virtuale inizializzerà tutte le variabili di istanza ai loro valori iniziali predefiniti. 3.c Terzo, la macchina virtuale invocherà il metodo nella classe Holder.

si prega di fare riferimento per quanto sopra: http://www.artima.com/designtechniques/initializationP.html

Supponi: il 1 ° thread inizia alle 10:00, chiama istato l'oggetto Holder effettuando la chiamata del nuovo Holer (42), 1) la macchina virtuale Java per prima cosa allocherà (almeno) abbastanza spazio sull'heap per contenere tutte le variabili di istanza dichiarate in Holder e le sue superclassi. - Sarà 10:01 tempo 2) In secondo luogo, la macchina virtuale inizializzerà tutte le variabili di istanza ai loro valori iniziali predefiniti - inizierà alle 10:02 3) Terzo, la macchina virtuale invocherà il metodo nella classe Holder .-- inizierà alle 10:04 ora

Ora Thread2 è iniziato su - > 10:02:01, e farà una chiamata assertSanity () 10:03, a quel punto n è stato inizializzato con il valore predefinito di Zero, il secondo thread legge i dati non aggiornati.

// pubblicazione non sicura Titolare del titolare pubblico;

se si rende il proprietario del Titolare finale pubblico risolverà questo problema

o

int privato; se si effettua la finale privata int n; risolverà questo problema.

fare riferimento: http: // www. cs.umd.edu/~pugh/java/memoryModel/jsr-133-faq.html nella sezione Come funzionano i campi finali con il nuovo JMM?

Anche io ero molto perplesso da quell'esempio. Ho trovato un sito Web che spiega a fondo l'argomento e i lettori potrebbero trovare utili: https: // www.securecoding.cert.org/confluence/display/java/TSM03-J.+Do+not+publish+partially+initialized+objects

Modifica: Il testo pertinente del link dice:

  

JMM consente ai compilatori di allocare memoria per il nuovo Helper   oggetto e assegnare un riferimento a quella memoria al campo helper   prima di inizializzare il nuovo oggetto Helper. In altre parole, il   il compilatore può riordinare la scrittura nel campo dell'istanza dell'helper e il file   scrivere che inizializza l'oggetto Helper (cioè this.n = n) in modo che   il primo si verifica per primo. Questo può esporre una finestra di gara durante la quale   altri thread possono osservare un oggetto Helper parzialmente inizializzato   esempio.

Autorizzato sotto: CC-BY-SA insieme a attribuzione
Non affiliato a StackOverflow
scroll top