java - tipos - ¿Por qué BufferedInputStream copia un campo a una variable local en lugar de usar el campo directamente?
variables de instancia java (4)
Creo que capturar la variable de clase
in
la
input
variable local es evitar un comportamiento incoherente si se cambia por otro hilo mientras se ejecuta
getInIfOpen()
.
Tenga en cuenta que el propietario de
in
es la clase padre y no lo marca como
final
.
Este patrón se replica en otras partes de la clase y parece ser una codificación defensiva razonable.
Cuando leo el código fuente de
java.io.BufferedInputStream.getInIfOpen()
, estoy confundido acerca de por qué escribió un código como este:
/**
* 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;
}
¿Por qué está usando el alias en lugar de usar la variable de campo directamente como se muestra a continuación?
/**
* 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;
}
¿Alguien puede dar una explicación razonable?
Este es un código tan corto, pero, en teoría, en un entorno multiproceso, puede cambiar justo después de la comparación, por lo que el método podría devolver algo que no comprobó (podría devolver
null
, haciendo exactamente lo que era destinado a prevenir).
Esto se debe a que la clase
BufferedInputStream
está diseñada para uso de subprocesos múltiples.
Aquí puede ver la declaración de
in
, que se coloca en la clase primaria
FilterInputStream
:
protected volatile InputStream in;
Como está
protected
, su valor puede ser cambiado por cualquier subclase de
FilterInputStream
, incluyendo
BufferedInputStream
y sus subclases.
Además, se declara
volatile
, lo que significa que si algún hilo cambia el valor de la variable, este cambio se reflejará inmediatamente en todos los otros hilos.
Esta combinación es mala, ya que significa que la clase
BufferedInputStream
no tiene forma de controlar o saber cuándo se cambia en.
Por lo tanto, el valor incluso se puede cambiar entre la comprobación de nulo y la declaración de retorno en
BufferedInputStream::getInIfOpen
, lo que efectivamente hace que la comprobación de nulo sea inútil.
Al leer el valor de
in
solo una vez para almacenarlo en caché en la
input
variable local, el método
BufferedInputStream::getInIfOpen
está a salvo de los cambios de otros subprocesos, ya que las variables locales siempre son propiedad de un solo subproceso.
Hay un ejemplo en
BufferedInputStream::close
, que se establece
in
nulo:
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()
}
}
Si otro hilo llama a
BufferedInputStream::close
mientras se ejecuta
BufferedInputStream::getInIfOpen
, esto daría como resultado la condición de carrera descrita anteriormente.
Si observa este código fuera de contexto, no hay una buena explicación para ese "alias". Es simplemente código redundante o estilo de código pobre.
Pero el contexto es que
BufferedInputStream
es una clase que puede subclasificarse y que necesita funcionar en un contexto de subprocesos múltiples.
La pista es que se declara en
FilterInputStream
está
protected volatile
.
Eso significa que existe la posibilidad de que una subclase pueda ingresar y asignar
null
a
in
.
Dada esa posibilidad, el "alias" está realmente allí para prevenir una condición de carrera.
Considere el código sin el "alias"
private InputStream getInIfOpen() throws IOException {
if (in == null)
throw new IOException("Stream closed");
return in;
}
-
El hilo A llama a
getInIfOpen()
-
El hilo A evalúa
in == null
y ve quein
no esnull
. -
El hilo B asigna
null
anull
. -
El hilo A ejecuta el
return in
. Lo cual devuelvenull
porquea
esvolatile
.
El "alias" evita esto.
Ahora se lee solo una vez por el subproceso A. Si el subproceso B asigna un
null
después del subproceso A, no importa.
El subproceso A generará una excepción o devolverá un valor no nulo (garantizado).