Pubblicazione di oggetti non thread-safe

Leggendo la concorrenza Java in pratica, sezione 3.5: Viene sollevato un reclamo

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

Oltre all’ovvio rischio in tutta sicurezza di creare 2 istanze di Holder, il libro afferma che può verificarsi un ansible problema di pubblicazione, oltre a una class 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!

Com’è ansible ? L’unica cosa a cui posso pensare che possa consentire un comportamento così ridicolo è se il costruttore di Holder non stia bloccando, quindi verrà creato un riferimento all’istanza mentre il codice costruttore viene ancora eseguito in un thread diverso. È ansible ?

Il motivo per cui questo è ansible è che Java ha un modello di memoria debole. Non garantisce l’ordine di lettura / scrittura. Questo particolare problema può essere riprodotto con i seguenti 2 snippet di codice che rappresentano 2 thread

Discussione 1:

 someStaticVariable = new Holder(42); 

Thread 2:

 someStaticVariable.assertSanity(); // can throw 

In apparenza sembra imansible che questo possa mai accadere. Per capire perché questo può accadere, devi superare la syntax Java e scendere ad un livello molto più basso. Se si guarda il codice per il thread 1, può essere essenzialmente suddiviso in una serie di scritture e allocazioni di memoria

  1. Alloca memoria a pointer1
  2. Scrivi 42 a pointer1 all’offset 0
  3. Scrivi pointer1 su someStaticVariable

Poiché Java ha un modello di memoria debole, è perfettamente ansible che il codice venga effettivamente eseguito nel seguente ordine dal punto di vista di thread2.

  1. Alloca memoria a pointer1
  2. Scrivi pointer1 su someStaticVariable
  3. Scrivi 42 a pointer1 all’offset 0

Spaventoso? Sì, ma può succedere.

Ciò significa che Thread2 può ora chiamare in assertSanity prima che n abbia ottenuto il valore 42. È ansible che il valore n sia letto due volte durante assertSanity, una volta prima che l’operazione # 3 sia completata e una volta dopo e quindi vedano 2 valori diversi e lanciare un’eccezione

MODIFICARE

Secondo Jon questo non è ansible (per fortuna) con le versioni più recenti di Java a causa degli aggiornamenti del modello di memoria.

EDIT 2nd

Secondo Jon, non dice mai che è imansible con l’ottava versione di Java a meno che il campo non sia definitivo.

Il modello di memoria Java utilizzato in modo tale che l’assegnazione al riferimento Holder potrebbe diventare visibile prima dell’assegnazione alla variabile all’interno dell’object.

Tuttavia, il modello di memoria più recente che ha avuto effetto a partire da Java 5 lo rende imansible, almeno per i campi finali: tutte le assegnazioni all’interno di un costruttore “accadono prima di” qualsiasi assegnazione del riferimento al nuovo object a una variabile. Consulta la sezione 17.4 della sezione relativa alla lingua di Java per maggiori dettagli, ma ecco lo snippet più pertinente:

Un object è considerato completamente inizializzato al termine del suo costruttore. Un thread che può vedere solo un riferimento a un object dopo che l’object è stato completamente inizializzato è garantito per vedere i valori correttamente inizializzati per i campi finali di quell’object

Quindi il tuo esempio potrebbe ancora fallire in quanto n non è definitivo, ma dovrebbe essere ok se fai n finale.

Ovviamente il:

 if (n != n) 

potrebbe certamente fallire per variabili non-finali, assumendo che il compilatore JIT non lo ottimizzi via – se le operazioni sono:

  • Recupera LHS: n
  • Recupera RHS: n
  • Confronta LHS e RHS

allora il valore potrebbe cambiare tra i due recuperi.

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

Il problema qui non è la class Holder stessa, ma il Titolare non è stato pubblicato correttamente. Tuttavia, Holder può essere reso immune da pubblicazioni improprie dichiarando definitivo il campo n, il che renderebbe Titolare immutabile; vedere la Sezione 3.5.2

E per il secondo blocco di codice:

Poiché la sincronizzazione non è stata utilizzata per rendere visibile il Titolare su altri thread, affermiamo che il Titolare non è stato pubblicato correttamente. Due cose possono andare storte con oggetti pubblicati in modo improprio. Altri thread potrebbero visualizzare un valore non aggiornato per il campo del titolare e pertanto visualizzare un riferimento null o un altro valore meno recente anche se è stato inserito un valore nel titolare. Ma molto peggio, altri thread potevano vedere un valore aggiornato per il riferimento del titolare, ma valori stantii per lo stato del Titolare. [16] Per rendere le cose ancora meno prevedibili, un thread potrebbe visualizzare un valore non aggiornato la prima volta che legge un campo e quindi un valore più aggiornato la volta successiva, motivo per cui assertSanity può lanciare AssertionError.

Penso che JaredPar lo abbia reso esplicito nel suo commento.

(Nota: non cercate voti qui – le risposte consentono di ottenere informazioni più dettagliate dei commenti).

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

 a = 1; b = 2; 

Se lo fai su un thread, un secondo thread può vedere b impostato su 2 prima che a sia impostato su 1. Inoltre, è ansible che ci sia una quantità illimitata di tempo tra un secondo thread e una di quelle variabili viene aggiornata e il altra variabile in corso di aggiornamento.

guardando questo da una prospettiva sana, se si assume che la dichiarazione

if(n != n)

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

Questo esempio rientra in “Un riferimento all’object contenente il campo finale non è sfuggito al costruttore”

Quando installi un nuovo object Holder con il nuovo operatore,

  1. la macchina virtuale Java prima allocherà (almeno) spazio sufficiente sullo heap per contenere tutte le variabili di istanza dichiarate in Holder e nelle 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 class Holder.

si prega di consultare sopra: http://www.artima.com/designtechniques/initializationP.html

Si supponga che: La prima discussione inizi alle 10:00, chiama instatied l’object Holder effettuando la chiamata di new Holer (42), 1) la Java virtual machine prima allocerà (almeno) spazio sufficiente sull’heap per contenere tutte le istanze variabili dichiarate in Holder e nelle 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à 10:02 tempo 3) Terzo, la macchina virtuale invocherà il metodo nella class Holder .– inizierà alle 10:04

Ora Thread2 è iniziato a -> 10:02:01 ora, e farà una chiamata assertSanity () alle 10:03, a quel tempo n è stato inizializzato con default di Zero, secondo thread che legge i dati non aggiornati.

// pubblicazione non sicura Public holder;

se rendi pubblico il titolare finale del Titolare risolverà questo problema

o

int privato; se si effettua l’int finale privato n; risolverà questo problema.

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

Ero anche 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+inizializzati+oggetti

Modifica: il testo pertinente dal link dice:

il JMM consente ai compilatori di allocare memoria per il nuovo object Helper e di assegnare un riferimento a quella memoria al campo helper prima di inizializzare il nuovo object Helper. In altre parole, il compilatore può riordinare la scrittura sul campo dell’istanza helper e la scrittura che inizializza l’object Helper (ovvero, this.n = n) in modo che il primo si verifichi per primo. Questo può esporre una finestra di gara durante la quale altri thread possono osservare un’istanza dell’object Helper parzialmente inizializzata.