Почему BufferedInputStream копирует поле в локальную переменную, а не напрямую использует поле

Когда я прочитал исходный код из java.io.BufferedInputStream.getInIfOpen(), я смущен тем, почему он написал такой код:

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

Почему он использует псевдоним вместо использования переменной поля in, как показано ниже:

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

Может ли кто-нибудь дать разумное объяснение?

Ответ 1

Если вы посмотрите на этот код вне контекста, нет никакого хорошего объяснения этому "псевдониму". Это просто избыточный код или плохой стиль кода.

Но контекст заключается в том, что BufferedInputStream - это класс, который может быть подклассифицирован и что он должен работать в многопоточном контексте.

Ключ в том, что in объявлен в FilterInputStream is protected volatile. Это означает, что есть шанс, что подкласс может достигнуть и назначить null на in. Учитывая эту возможность, "псевдоним" на самом деле существует, чтобы предотвратить состояние гонки.

Рассмотрим код без "псевдонима"

private InputStream getInIfOpen() throws IOException {
    if (in == null)
        throw new IOException("Stream closed");
    return in;
}
  • Тема A вызывает getInIfOpen()
  • Thread A оценивает in == null и видит, что in не null.
  • Thread B назначает null на in.
  • Thread A выполняет return in. Что возвращает null, потому что a является volatile.

"Псевдоним" предотвращает это. Теперь in читается только один раз по потоку A. Если поток B назначает null после того, как поток A имеет in, это не имеет значения. Thread A либо генерирует исключение, либо возвращает (гарантированное) ненулевое значение.

Ответ 2

Это связано с тем, что класс BufferedInputStream предназначен для многопоточного использования.

Здесь вы видите объявление in, которое помещается в родительский класс FilterInputStream:

protected volatile InputStream in;

Так как это protected, его значение может быть изменено любым подклассом FilterInputStream, включая BufferedInputStream и его подклассы. Кроме того, он объявляется volatile, что означает, что если какой-либо поток изменяет значение переменной, это изменение сразу будет отражено во всех других потоках. Эта комбинация плохая, так как это означает, что класс BufferedInputStream не может контролировать или знать, когда изменяется in. Таким образом, значение может быть даже изменено между проверкой null и оператором return в BufferedInputStream::getInIfOpen, что фактически делает проверку на бесполезность. Читая значение in только один раз, чтобы кэшировать его в локальной переменной input, метод BufferedInputStream::getInIfOpen безопасен для изменений от других потоков, поскольку локальные переменные всегда принадлежат одному потоку.

В BufferedInputStream::close есть пример, который устанавливает in в 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()
    }
}

Если BufferedInputStream::close вызывается другим потоком, пока выполняется BufferedInputStream::getInIfOpen, это приведет к описанному выше состоянию гонки.

Ответ 3

Это такой короткий код, но, теоретически, в многопоточной среде in может измениться сразу после сравнения, поэтому метод может вернуть что-то, что он не проверял (он мог бы вернуть null, тем самым делая то, что он должен был предотвратить).

Ответ 4

Я считаю, что захват переменной класса in локальной переменной input заключается в предотвращении несогласованного поведения, если in изменяется другим потоком, пока выполняется getInIfOpen().

Обратите внимание, что владелец in является родительским классом и не указывает его как final.

Этот шаблон реплицируется в других частях класса и представляется разумным защитным кодированием.