为什么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直接使用field变量,如下所示:

/**
 * 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 '16

@rkosegi在我的Eclipse版本上,问题与类似。可能不是很常见的情况。而且,无论如何,我并不是故意这样说的(显然是个坏笑话)。:)
Debosmit Ray

Answers:


119

如果您是从上下文中看这段代码,则没有关于“别名”的良好解释。它仅仅是冗余代码或不良的代码样式。

但是上下文是BufferedInputStream可以被子类化的类,它需要在多线程上下文中工作。

线索是,in在宣布FilterInputStreamIS protected volatile。这意味着子类有可能进入并分配nullin。考虑到这种可能性,实际上会使用“别名”来防止出现竞争状况。

考虑不带“别名”的代码

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分配nullin
  4. 线程A执行return innull之所以返回,a是因为volatile

“别名”可以防止这种情况。现在in,线程A仅读取一次。如果线程B null在线程A之后分配in,则没有关系。线程A将抛出异常或返回(保证的)非空值。


11
这说明了为什么protected变量在多线程上下文中是邪恶的。
米克助记符

2
的确如此。但是,AFAIK这些类可以一直追溯到Java 1.0。这只是糟糕的设计决策的另一个例子,由于担心破坏客户代码而无法解决。
史蒂芬·C

2
@StephenC感谢您的详细说明+1。这是否意味着,protected如果代码是多线程的,就不应该在代码中使用变量?
Madhusudana Reddy Sunnapu,2016年

3
@MadhusudanaReddySunnapu总的教训是,在多个线程可以访问同一状态的环境中,您需要以某种方式控制该访问。那可能是只能通过setter访问的私有变量,也可能是像这样的局部保护措施,也可能是以线程安全的方式使变量一次写入。
克里斯·海斯

3
@sam-1)不需要解释所有比赛条件和状态。答案的目的是指出为什么实际上需要这种看似莫名其妙的代码。2)如何?
史蒂芬·C

20

这是因为该类BufferedInputStream专为多线程使用而设计。

在这里,您可以看到的声明in,该声明位于父类中FilterInputStream

protected volatile InputStream in;

既然是protected,则其值可以由的任何子类(FilterInputStream包括BufferedInputStream及其子类)更改。同样,声明了volatile,这意味着,如果任何线程更改了变量的值,则此更改将立即反映在所有其他线程中。这种组合不好,因为这意味着该类BufferedInputStream无法控制或知道何时in更改。因此,甚至可以在检查null和in中的return语句之间更改该值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::closeBufferedInputStream::getInIfOpen执行时被另一个线程调用,则将导致上述竞争状态。


我同意,因为我们看到的东西一样compareAndSet()CAS在代码和注释等。我还搜索了BufferedInputStream代码,发现了很多synchronized方法。因此,它旨在用于多线程,尽管我确信从未以这种方式使用过。无论如何,我认为您的答案是正确的!
sparc_spread

这可能是有道理的,因为getInIfOpen()只能从中public synchronized调用BufferedInputStream
米克助记符

6

这是一个短代码,但是从理论上讲,在多线程环境中,in比较之后可能会立即更改,因此该方法可以返回未检查的内容(可以返回null,因此可以执行原本打算防止)。


如果我说引用in 可能在调用方法的时间和返回值之间(在多线程环境中)发生变化,我是否正确?
Debosmit Ray

是的,你可以这么说。最终,可能性实际上将取决于具体情况(我们所知道的唯一事实是in随时可能发生变化)。
acdcjunior

4

我相信捕获类变量in到局部变量input是为了防止ingetInIfOpen()运行时被另一个线程更改时行为不一致。

请注意,的所有者in是父类,并且不会将其标记为final

该模式在该类的其他部分中被复制,并且似乎是合理的防御性编码。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.