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

107

Когда я читаю исходный код 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;
}

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

Святой
источник
В Eclipse, вы не можете приостановить отладчик для ifоператора. Может быть причиной этой переменной псевдонима. Просто хотел выбросить это там. Я, конечно, размышляю.
Debosmit Ray
@DebosmitRay: Неужели нельзя сделать паузу в ifзаявлении?
rkosegi
@rkosegi В моей версии Eclipse проблема похожа на эту . Возможно, это не очень частое явление. И в любом случае, я не имел в виду это на легкой ноте (явно плохая шутка). :)
Debosmit Ray

Ответы:

119

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

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

Подсказка в том, что inзаявлено в FilterInputStreamis protected volatile. Это означает, что есть шанс, что подкласс может проникнуть внутрь и назначить nullего in. Учитывая такую ​​возможность, "псевдоним" на самом деле существует для предотвращения состояния гонки.

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

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

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

Стивен С
источник
11
Это показывает, почему protectedпеременные - зло в многопоточном контексте.
Mick Mnemonic
2
Это действительно так. Однако, AFAIK эти классы восходят к Java 1.0. Это всего лишь еще один пример неудачного дизайнерского решения, которое невозможно исправить из-за страха нарушить код клиента.
Stephen C
2
@StephenC Спасибо за подробное объяснение +1. Значит ли это, что мы не должны использовать protectedпеременные в нашем коде, если он многопоточный?
Мадхусудана Редди Суннапу
3
@MadhusudanaReddySunnapu Общий урок состоит в том, что в среде, где несколько потоков могут обращаться к одному и тому же состоянию, вам нужно как-то контролировать этот доступ. Это может быть частная переменная, доступная только через сеттер, это может быть локальная защита, подобная этой, это может быть сделано путем однократной записи переменной потокобезопасным способом.
Chris Hayes
3
@sam - 1) Не нужно объяснять все условия и состояния гонки. Цель ответа - указать, почему этот, казалось бы, необъяснимый код на самом деле необходим. 2) Как так?
Stephen C
20

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

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

protected volatile InputStream in;

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

Стефан Долласе
источник
Я согласен, так как мы видим вещи , как compareAndSet(), CASи т.д. в коде и в комментариях. Я также поискал BufferedInputStreamкод и нашел множество synchronizedметодов. Итак, он предназначен для многопоточного использования, хотя я никогда не использовал его таким образом. Во всяком случае, я думаю, что ваш ответ правильный!
sparc_spread
Вероятно, это имеет смысл, потому что getInIfOpen()вызывается только из public synchronizedметодов BufferedInputStream.
Mick Mnemonic
6

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

acdcjunior
источник
Правильно ли я говорю, что ссылка in может измениться между моментом вызова метода и возвратом значения (в многопоточной среде)?
Debosmit Ray
Да, можно так сказать. В конечном итоге вероятность действительно будет зависеть от конкретного случая (все, что мы знаем наверняка, это то, что inможет измениться в любой момент).
acdcjunior
4

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

Обратите внимание, что владельцем inявляется родительский класс, и он не помечается как final.

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

Сэм
источник