我在java.io.PipedInputStream中发现了一个错误吗?

时间:2015-02-19 21:23:08

标签: java

我不确定,但我很确定我在Oracle Java实现中发现了一个错误(或一个未记录的功能)(1.7.0_67和1.8.0_31我可以验证为受影响)。

症状

当管道已满时,对管道的写入可能会比管道再次释放所需的时间长一秒钟。这个问题的一个最小例子如下(我把这里显示的样本推到了a repository on GitHub):

private static void threadA() throws IOException, InterruptedException {
  logA("Filling pipe...");
  pos.write(new byte[5]);
  logA("Pipe full. Writing one more byte...");
  pos.write(0);
  logA("Done.");
}

private static void threadB() throws IOException, InterruptedException {
  logB("Sleeping a bit...");
  Thread.sleep(100);
  logB("Making space in pipe...");
  pis.read();
  logB("Done.");
}

pispos分别与PipedInputStreamPipedOutputStream实例相关联。 logAlogB是辅助函数,它们输出线程名称(A或B),时间戳(以毫秒为单位)和消息。输出如下:

     0 A: Filling pipe...
     6 B: Sleeping a bit...
     7 A: Pipe full. Writing one more byte...
   108 B: Making space in pipe...
   109 B: Done.
  1009 A: Done.

可以看出,B: DoneA: Done之间有一秒(1000毫秒)。这是由Oracle Java 1.7.0_67中PipedInputStream的实现引起的,如下所示:

private void awaitSpace() throws IOException {
    while (in == out) {
        checkStateForReceive();

        /* full: kick any waiting readers */
        notifyAll();
        try {
            wait(1000);
        } catch (InterruptedException ex) {
            throw new java.io.InterruptedIOException();
        }
    }
}

只有在达到超时(1000毫秒,如上所示)或调用wait(1000)时才会中断notifyAll(),这只会在以下情况下发生:

  • awaitSpace()之前wait(1000)之前,正如我们在上面的代码段中看到的那样
  • receivedLast()中,当流关闭时调用(此处不适用)
  • read(),但仅当read()正在等待空缓冲区填满时 - 此处也不适用

问题

有没有人有足够的Java经验告诉我这是否应该是预期的行为? awaitSpace()使用方法PipedOutputStream.write(...)等待可用空间,他们的合同只是声明:

  

此方法将阻塞,直到所有字节都写入输出流。

虽然严格不违反,但1秒的等待时间似乎很长。如果我要修复此问题(最小化/减少等待时间),我建议在每次读取结束时插入notifyAll()以确保等待的作者得到通知。为了避免额外的同步时间开销,可以使用一个简单的布尔标志(并且不会损害线程安全性)。

受影响的Java版本

到目前为止,我可以在Java 7和Java 8上验证这一点,确切地说是以下版本:

$ java -version
java version "1.7.0_67"
Java(TM) SE Runtime Environment (build 1.7.0_67-b01)
Java HotSpot(TM) 64-Bit Server VM (build 24.65-b04, mixed mode)

$ java -version
java version "1.8.0_31"
Java(TM) SE Runtime Environment (build 1.8.0_31-b13)
Java HotSpot(TM) 64-Bit Server VM (build 25.31-b07, mixed mode)

1 个答案:

答案 0 :(得分:13)

这是[{1}}中众所周知的问题,最终解决方案(针对JDK-8014239)未得到解决#34;

  

JDK-4545831 PipedInputStream性能问题

     

缓冲区时读取的类块   空和写入块然后缓冲区已满。它会阻挡   呼叫等待(1000),然而读者只会被作家吵醒   遇到一个完整的缓冲区(或等待超时),只有一个作家   被遇到空缓冲区的读者(或等待时间)吵醒   出)。

     
    

客户解决方法:在每次read()/ write()之后Notify()通知PipedInputStream可能会解决问题,但仍会导致     由于许多不必要的notify()调用正在进行,因此性能欠佳     制成。

  

  

JDK-4404700 PipedInputStream由于轮询而太慢(建议使用alt实现)

     

java.io.PipedInputStream太慢,因为它轮询以检查新数据。   它每秒都会测试新数据是否可用。当数据可用时   可能浪费了近一秒钟。它还有一个不可设置的小缓冲区。   我建议考虑PipedInputStream和的以下实现   PipedOutputStream,更简单,更快。

     
    

BT2:评价

         

我们应该把它作为梅林和老虎的机会目标。由于提交的代码的类的年龄设计     替换,使用它可能存在兼容性问题。

  

  

JDK-8014239 PipedInputStream未通知等待读者

     

当从PipedInputStream / PipedOutputStream对读/写时,当新数据写入PipedOutputStream时,read()会准确地阻塞一秒钟。原因是PipedInputStream只唤醒等待的读者,当在receive()期间填充缓冲区时。   解决方案非常简单,在PipedInputStream中的两个receive()方法的末尾添加一个notifyAll()。

     
    

大多数现实生活场景如何从中获益并不明显     建议改变。每次写入通知可能会导致不必要的编写者     摊位。从而打破了管道 - 时间 - 解耦的主要目的之一     来自作家和缓冲的读者。     PipedInputStream / PipedWriter API为我们提供了一种灵活的方式来控制频率     我们希望读者收到有关新数据的通知。即,flush()。调用     flush()在适当的时候我们可以控制延迟和吞吐量。