Perché BufferedInputStream copia un campo in una variabile locale invece di utilizzare il campo direttamente


107

Quando ho letto il codice sorgente da java.io.BufferedInputStream.getInIfOpen(), sono confuso sul motivo per cui ha scritto codice come questo:

/**
 * Check to make sure that underlying input stream has not been
 * nulled out due to close; if not return it;
 */
private InputStream getInIfOpen() throws IOException {
    InputStream input = in;
    if (input == null)
        throw new IOException("Stream closed");
    return input;
}

Perché utilizza l'alias invece di utilizzare indirettamente la variabile di campo come di seguito:

/**
 * Check to make sure that underlying input stream has not been
 * nulled out due to close; if not return it;
 */
private InputStream getInIfOpen() throws IOException {
    if (in == null)
        throw new IOException("Stream closed");
    return in;
}

Qualcuno può dare una spiegazione ragionevole?


In Eclipse, non puoi mettere in pausa un debugger su ifun'istruzione. Potrebbe essere una ragione per quella variabile alias. Volevo solo buttarlo là fuori. Io speculo, ovviamente.
Debosmit Ray

@DebosmitRay: Davvero non puoi fermarti sulla ifdichiarazione?
rkosegi

@rkosegi Nella mia versione di Eclipse, il problema è simile a questo . Potrebbe non essere un evento molto comune. E comunque, non lo intendevo su una nota leggera (chiaramente un brutto scherzo). :)
Debosmit Ray

Risposte:


119

Se guardi questo codice fuori contesto, non c'è una buona spiegazione per questo "alias". È semplicemente un codice ridondante o uno stile di codice scadente.

Ma il contesto è che BufferedInputStreamè una classe che può essere sottoclasse e che deve funzionare in un contesto multi-thread.

L'indizio è che inè dichiarato in FilterInputStreamis protected volatile. Ciò significa che c'è una possibilità che una sottoclasse potrebbe raggiungere in e assegnare nulla in. Data questa possibilità, l '"alias" è effettivamente lì per prevenire una condizione di competizione.

Considera il codice senza "alias"

private InputStream getInIfOpen() throws IOException {
    if (in == null)
        throw new IOException("Stream closed");
    return in;
}
  1. Il thread A chiama getInIfOpen()
  2. Il thread A valuta in == nulle vede che innon lo è null.
  3. Thread B assegna nulla in.
  4. Il thread A viene eseguito return in. Che ritorna nullperché aè un file volatile.

L '"alias" lo impedisce. Ora inviene letto solo una volta dal thread A. Se il thread B viene assegnato nulldopo il thread A in, non ha importanza. Il thread A genererà un'eccezione o restituirà un valore non nullo (garantito).


11
Il che mostra perché le protectedvariabili sono dannose in un contesto multi-thread.
Mick Mnemonic

2
Lo fa davvero. Tuttavia, per quanto ne so, queste classi risalgono a Java 1.0. Questo è solo un altro esempio di una cattiva decisione di progettazione che non è stato possibile correggere per paura di violare il codice del cliente.
Stephen C

2
@StephenC Grazie per la spiegazione dettagliata +1. Quindi questo significa che non dovremmo usare protectedvariabili nel nostro codice se è multi-thread?
Madhusudana Reddy Sunnapu

3
@MadhusudanaReddySunnapu La lezione generale è che in un ambiente in cui più thread possono accedere allo stesso stato, è necessario controllare tale accesso in qualche modo. Potrebbe essere una variabile privata accessibile solo tramite setter, potrebbe essere una guardia locale come questa, potrebbe essere rendendo la variabile write-once in modo sicuro per i thread.
Chris Hayes

3
@sam - 1) Non è necessario spiegare tutte le condizioni e gli stati di gara. Lo scopo della risposta è sottolineare perché questo codice apparentemente inspiegabile è in realtà necessario. 2) In che modo?
Stephen C

20

Questo perché la classe BufferedInputStreamè progettata per l'utilizzo multi-thread.

Qui, vedi la dichiarazione di in, che si trova nella classe genitore FilterInputStream:

protected volatile InputStream in;

Poiché lo è protected, il suo valore può essere modificato da qualsiasi sottoclasse di FilterInputStream, incluse BufferedInputStreame dalle sue sottoclassi. Inoltre, è dichiarato volatile, il che significa che se un thread cambia il valore della variabile, questa modifica si rifletterà immediatamente in tutti gli altri thread. Questa combinazione è cattiva, poiché significa che la classe BufferedInputStreamnon ha modo di controllare o sapere quando inviene modificata. Pertanto, il valore può anche essere modificato tra il controllo per null e l'istruzione return in BufferedInputStream::getInIfOpen, il che rende effettivamente inutile il controllo per null. Leggendo il valore di inuna sola volta per memorizzarlo nella variabile locale input, il metodo BufferedInputStream::getInIfOpenè sicuro contro le modifiche da altri thread, poiché le variabili locali sono sempre di proprietà di un singolo thread.

C'è un esempio in BufferedInputStream::close, che viene impostato insu null:

public void close() throws IOException {
    byte[] buffer;
    while ( (buffer = buf) != null) {
        if (bufUpdater.compareAndSet(this, buffer, null)) {
            InputStream input = in;
            in = null;
            if (input != null)
                input.close();
            return;
        }
        // Else retry in case a new buf was CASed in fill()
    }
}

Se BufferedInputStream::closeviene chiamato da un altro thread mentre BufferedInputStream::getInIfOpenviene eseguito, ciò comporterebbe la condizione di competizione descritta sopra.


Sono d'accordo, in quanto stiamo vedendo le cose come compareAndSet(), CASecc nel codice e nei commenti. Ho anche cercato il BufferedInputStreamcodice e ho trovato numerosi synchronizedmetodi. Quindi, è inteso per l'uso multi-thread, anche se di sicuro non l'ho mai usato in questo modo. Comunque, penso che la tua risposta sia corretta!
sparc_spread

Questo probabilmente ha senso perché getInIfOpen()viene chiamato solo dai public synchronizedmetodi di BufferedInputStream.
Mick Mnemonic

6

Questo è un codice così breve, ma, in teoria, in un ambiente multi-thread, inpotrebbe cambiare subito dopo il confronto, quindi il metodo potrebbe restituire qualcosa che non ha controllato (potrebbe restituire null, facendo così esattamente la cosa a cui era destinato impedire).


Ho ragione se dico che il riferimento in potrebbe cambiare tra il momento in cui chiami il metodo e la restituzione del valore (in un ambiente multi-thread)?
Debosmit Ray

Sì, puoi dirlo. In definitiva, la probabilità dipenderà davvero dal caso concreto (tutto ciò che sappiamo per certo è che inpuò cambiare in qualsiasi momento).
acdcjunior

4

Credo che catturare la variabile di classe nella variabile inlocale serva inputa prevenire comportamenti incoerenti se inviene modificato da un altro thread mentre getInIfOpen()è in esecuzione.

Si noti che il proprietario di inè la classe genitore e non lo contrassegna come final.

Questo modello è replicato in altre parti della classe e sembra essere un codice difensivo ragionevole.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.