Por que BufferedInputStream copia um campo para uma variável local em vez de usar o campo diretamente


107

Quando leio o código-fonte do java.io.BufferedInputStream.getInIfOpen(), fico confuso sobre por que ele escreveu um 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 que está usando o alias em vez de usar a variável de campo indiretamente como abaixo:

/**
 * 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;
}

Alguém pode dar uma explicação razoável?


Em Eclipse, você não pode pausar um depurador em uma ifinstrução. Pode ser um motivo para essa variável de alias. Só queria jogar isso lá fora. Eu especulo, é claro.
Debosmit Ray

@DebosmitRay: Realmente não pode fazer uma pausa na ifdeclaração?
rkosegi

@rkosegi Na minha versão do Eclipse, o problema é semelhante a este . Pode não ser uma ocorrência muito comum. E de qualquer maneira, eu não quis dizer isso de forma leve (claramente uma piada de mau gosto). :)
Debosmit Ray

Respostas:


119

Se você olhar para este código fora do contexto, não há uma boa explicação para esse "alias". É simplesmente código redundante ou estilo de código pobre.

Mas o contexto é que BufferedInputStreamé uma classe que pode ser subclassificada e que precisa funcionar em um contexto multi-thread.

A pista é que inestá declarado em FilterInputStreamis protected volatile. Isso significa que há uma chance de que uma subclasse possa alcançar e atribuir nulla in. Dada essa possibilidade, o "alias" está realmente lá para evitar uma condição de corrida.

Considere o código sem o "alias"

private InputStream getInIfOpen() throws IOException {
    if (in == null)
        throw new IOException("Stream closed");
    return in;
}
  1. Thread A chama getInIfOpen()
  2. O tópico A avalia in == nulle vê que innão é null.
  3. A linha B é atribuída nulla in.
  4. O thread A é executado return in. Que retorna nullporque aé a volatile.

O "alias" impede isso. Agora iné lido apenas uma vez pelo encadeamento A. Se o encadeamento B for atribuído nullapós o encadeamento A ter in, não importa. O thread A lançará uma exceção ou retornará um valor não nulo (garantido).


11
O que mostra por que as protectedvariáveis ​​são ruins em um contexto multithread.
Mick Mnemonic

2
É verdade. No entanto, essas classes AFAIK remontam ao Java 1.0. Este é apenas mais um exemplo de uma decisão de design ruim que não pôde ser corrigida por medo de quebrar o código do cliente.
Stephen C

2
@StephenC Obrigado pela explicação detalhada +1. Então, isso significa que não devemos usar protectedvariáveis ​​em nosso código se ele for multi-threaded?
Madhusudana Reddy Sunnapu

3
@MadhusudanaReddySunnapu A lição geral é que em um ambiente onde vários threads podem acessar o mesmo estado, você precisa controlar esse acesso de alguma forma. Pode ser uma variável privada acessível apenas via setter, pode ser um guarda local como este, pode ser tornando a variável write-once de uma maneira threadsafe.
Chris Hayes

3
@sam - 1) Não precisa explicar todas as condições e estados da corrida. O objetivo da resposta é apontar por que esse código aparentemente inexplicável é de fato necessário. 2) Como assim?
Stephen C

20

Isso ocorre porque a classe BufferedInputStreamfoi projetada para uso multi-thread.

Aqui, você vê a declaração de in, que é colocada na classe pai FilterInputStream:

protected volatile InputStream in;

Visto que é protected, seu valor pode ser alterado por qualquer subclasse de FilterInputStream, including BufferedInputStreame suas subclasses. Além disso, é declarado volatile, o que significa que se qualquer segmento alterar o valor da variável, essa alteração será imediatamente refletida em todos os outros segmentos. Essa combinação é ruim, pois significa que a classe BufferedInputStreamnão tem como controlar ou saber quando iné alterada. Assim, o valor pode até ser alterado entre a verificação de nulo e a instrução de retorno em BufferedInputStream::getInIfOpen, o que efetivamente torna a verificação de nulo inútil. Ao ler o valor de inapenas uma vez para armazená-lo em cache na variável local input, o método BufferedInputStream::getInIfOpené seguro contra alterações de outras threads, uma vez que as variáveis ​​locais sempre pertencem a uma única thread.

Há um exemplo em BufferedInputStream::close, que define incomo 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()
    }
}

Se BufferedInputStream::closefor chamado por outro encadeamento enquanto BufferedInputStream::getInIfOpené executado, isso resultaria na condição de corrida descrita acima.


Concordo, na medida em que nós estamos vendo coisas como compareAndSet(), CAS, etc. no código e nos comentários. Também pesquisei o BufferedInputStreamcódigo e encontrei vários synchronizedmétodos. Portanto, ele se destina ao uso multi-threaded, embora eu nunca tenha usado dessa forma. Enfim, acho que sua resposta está correta!
sparc_spread

Isso provavelmente faz sentido porque getInIfOpen()só é chamado de public synchronizedmétodos de BufferedInputStream.
Mick Mnemonic

6

Este é um código tão curto, mas, teoricamente, em um ambiente multi-threaded, inpode mudar logo após a comparação, então o método pode retornar algo que não foi verificado (ele pode retornar null, fazendo exatamente o que foi feito para evita).


Estou correto se digo que a referência in pode mudar entre o momento em que você chama o método e o retorno do valor (em um ambiente multi-thread)?
Debosmit Ray

Sim, você pode dizer isso. Em última análise, a probabilidade realmente dependerá do caso concreto (tudo o que sabemos com certeza é que inpode mudar a qualquer momento).
acdcjunior

4

Acredito que capturar a variável de classe inpara a variável local inputé evitar um comportamento inconsistente se infor alterado por outro segmento durante a getInIfOpen()execução.

Observe que o proprietário de iné a classe pai e não a marca como final.

Esse padrão é replicado em outras partes da classe e parece ser uma codificação defensiva razoável.

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.