2016-02-14 1 views
14

Sezione 3.2.1 di "Java Concurrency in Practice" di Goetz contiene la seguente regola:Java: sicuro "leak" questo riferimento nel costruttore per la classe finale tramite _happens-before_ relation?

Non lasciare che il riferimento this di fuoriuscire durante la costruzione

Capisco che, in generale, permettendo this la fuga può portare ad altri thread che vedono versioni del tuo oggetto costruite in modo incompleto e violare la garanzia di sicurezza dell'inizializzazione dei campi final (come ad esempio here)

Ma è mai possibile perdere in sicurezza this? In particolare, se si stabilisce una relazione happen-before prima della perdita?

Ad esempio, il official Executor Javadoc dice

azioni in un filo prima di presentare un oggetto Runnable a un Executoraccade-prima sua esecuzione inizia, forse in un altro thread

My comprensione ingenua della lettura del modello di memoria Java questo è che qualcosa come il seguente dovrebbe essere sicuro, anche se perde this prima della fine del costruttore:

public final class Foo { 
    private final String str1; 
    private String str2; 
    public Foo(Executor ex) { 
    str1 = "I'm final"; 
    str2 = "I'm not"; 
    ex.execute(new Runnable() { 
     // Oops: Leakage! 
     public void run() { System.out.println(str1 + str2);} 
    }); 
    } 
} 

Cioè, anche se abbiamo fatto trapelare this a un potenzialmente dannoso Executor, le assegnazioni a str1 e str2accadere, prima la perdita, per cui l'oggetto è (per tutti gli effetti) completamente costruiti, anche se non è stato "completamente inizializzato" per JLS 17.5.

Si noti che anch'io sto richiedendo che la classe sia final, poiché i campi di ogni sottoclasse verrebbero inizializzati dopo la perdita.

Mi manca qualcosa qui? Questo è effettivamente garantito per essere ben educato? Mi sembra un esempio legittimo di "Piggybacking on synchronization" (16.1.4) In generale, apprezzerei molto ogni suggerimento su risorse aggiuntive in cui questi problemi sono trattati.

EDIT: Sono consapevole che, come osservato da @jtahlborn, posso evitare il problema utilizzando una fabbrica statica pubblica. Sto cercando una risposta alla domanda direttamente per consolidare la mia comprensione del modello di memoria Java.

EDIT # 2: This answer allude a quello che sto cercando di arrivare. Cioè, seguendo la regola del JLS ivi citata è sufficiente per garantire la visibilità di tutti i campi final. Ma è necessario, oppure possiamo fare uso di altri meccanismi - prima del per garantire le nostre garanzie di visibilità?

+0

in base al libro, la ragione è "un oggetto è in uno stato prevedibile e coerente solo dopo il costruttore _returns_" (enfasi il mio). la costruzione degli oggetti è garantita per essere completa solo quando il costruttore ritorna, quindi la relazione tra prima e l'evento è troppo presto (deve essere _dopo che il costruttore restituisce). – jtahlborn

+0

Concordo con la vostra enfasi qui - le garanzie integrate circa, ad es. la semantica 'finale' richiede di tornare prima dal costruttore. Ma la mia domanda è quali proprietà specifiche vengono abbandonate non aspettando fino a quel momento? Una lettura è che la variabile automagica "le variabili finali sono garantite per essere inizializzate in modo sicuro" non è più garantita per te, ma puoi comunque affermare manualmente l'ordine tramite le barriere della memoria prima o esplicite (se tali cose esistono in Java). Un'altra lettura è, beh, demoni nasali. Sto cercando di trovare una spiegazione più precisa, – Tom

+1

, l'accadere-prima sembrerebbe garantire che il normale compito dei membri sia "fatto". tuttavia, per me, la creazione di oggetti coinvolge una serie di altri processi "interni" (allocazione della memoria, gestione della gestione di jvm, mutex interno, ecc.). non è garantito che un oggetto sia completamente valido fino al ritorno del costruttore. onestamente, non sono davvero sicuro di cosa stai cercando qui. la specifica dice "non sei garantito" e ciò consente a jvm implementor di fare tutto ciò che vuole fino al completamento del costruttore. solo perché "potrebbe funzionare" su un dato jvm non significa nulla. – jtahlborn

risposta

4

Sei corretto. Nel generale, il modello di memoria Java non tratta i costruttori in alcun modo speciale.Pubblicare un riferimento a un oggetto prima o dopo l'uscita di un costruttore fa davvero poca differenza.

L'unica eccezione è, ovviamente, per i campi final. L'uscita di un costruttore in cui è scritto un campo finale per definire un'azione di "blocco" sul campo; se this viene pubblicato dopo lo freeze, anche senza i bordi dei punti precedenti, gli altri thread leggeranno il campo correttamente inizializzato; ma non se this è pubblicato prima dello freeze.

È interessante notare che, se esiste la concatenazione del costruttore, freeze è definito sull'ambito più piccolo; per esempio.

-- class Bar 

final int x; 

Bar(int x, int ignore) 
{ 
    this.x = x; // assign to final 
} // [f] freeze action on this.x 

public Bar(int x) 
{ 
    this(x, 0); 
    // [f] is reached! 
    leak(this); 
} 

Qui leak(this) è sicuro w.r.t. this.x.

Vedere il mio altro answer per ulteriori dettagli sui campi final.


Se final sembra troppo complicato, lo è. Il mio consiglio è - dimenticalo! Non fare mai affidamento sulla semantica dei campi final per pubblicare messaggi non sicuri. Se il programma è correttamente sincronizzato, non è necessario preoccuparsi dei campi final o della loro delicata semantica. Sfortunatamente, il clima attuale è quello di spingere i campi final il più possibile, creando un'indebita pressione sui programmatori.

+0

Grazie per entrambe le risposte! Quindi, se ho capito bene: posso contare sul fatto che accade normalmente, prima degli ordini, come credevo. Tuttavia, questo * fa * significa che se pubblicherò un 'Foo' non sicuro, potrei vedere un'inizializzazione impropria. per esempio. se quell'Esecutore usa l'istanza per compilare una variabile membro 'finale Foo' in qualche wrapper, allora gli utenti di quel wrapper potrebbero vedere un 'nullo' (sebbene gli utenti che * fanno * vedano non null vedrebbero Stringhe correttamente inizializzate). È corretto? – Tom

+0

Se 'Foo' perde se stesso (' pippo') nel costruttore su 'ex', e' ex' vede 'pippo' attraverso il normale accade prima, e' ex' crea 'w = nuovo W (pippo)', in cui 'W.foo' è' final', e quindi 'ex' pubblica' w' non sicuro (ad esempio attraverso una variabile statica non volatile), e un altro thread legge 'w' non-null, ---- che thread vedrà 'w.foo',' w.foo.str1', 'w.foo.str2' sono tutti inizializzati correttamente. – ZhongYu