为什么没有更多的Java代码使用PipedInputStream / PipedOutputStream?

时间:2009-01-27 16:35:43

标签: java design-patterns concurrency pipe

我最近发现了这个成语,我想知道是否有我遗失的东西。我从来没有见过它。我在野外工作的几乎所有Java代码都倾向于将数据压入字符串或缓冲区,而不是像这个例子(例如使用HttpClient和XML API):

    final LSOutput output; // XML stuff initialized elsewhere
    final LSSerializer serializer;
    final Document doc;
    // ...
    PostMethod post; // HttpClient post request
    final PipedOutputStream source = new PipedOutputStream();
    PipedInputStream sink = new PipedInputStream(source);
    // ...
    executor.execute(new Runnable() {
            public void run() {
                output.setByteStream(source);
                serializer.write(doc, output);
                try {
                    source.close();
                } catch (IOException e) {
                    throw new RuntimeException(e);
                }
            }});

    post.setRequestEntity(new InputStreamRequestEntity(sink));
    int status = httpClient.executeMethod(post);

该代码使用Unix管道样式技术来防止XML数据的多个副本保留在内存中。它使用HTTP Post输出流和DOM Load / Save API将XML Document序列化为HTTP请求的内容。据我所知,它使用非常少的额外代码(仅RunnablePipedInputStreamPipedOutputStream的几行)最小化了内存的使用。

那么,这个成语有什么问题?如果这个成语没有任何问题,为什么我没有看到它?

编辑:澄清一下,PipedInputStreamPipedOutputStream取代了遍布各地的样板缓冲区副本,它们还允许您在写出已处理数据的同时处理传入数据。他们不使用操作系统管道。

9 个答案:

答案 0 :(得分:44)

来自Javadocs

  

通常,一个线程从PipedInputStream对象读取数据,而其他线程将数据写入相应的PipedOutputStream。建议不要尝试使用单个线程中的两个对象,因为它可能使线程死锁。

这可能部分解释了为什么它不常用。

我认为另一个原因是许多开发人员不了解其目的/利益。

答案 1 :(得分:7)

在你的例子中,你创建了两个线程来完成一个可以完成的工作。并将I / O延迟引入混合中。

你有更好的例子吗?或者我刚回答你的问题。


将一些评论(至少我对它们的看法)提取到主要回复中:

  • 并发性将复杂性引入应用程序。您现在必须关注独立数据流的排序,而不是处理单个线性数据流。在某些情况下,增加的复杂性可能是合理的,特别是如果您可以利用多个内核/ CPU来执行CPU密集型工作。
  • 如果您处于可以从并发操作中受益的情况,通常有一种更好的方法来协调线程之间的数据流。例如,使用并发队列在线程之间传递对象,而不是将管道流包装在对象流中。
  • 当一个管道流可能是一个很好的解决方案时,当你有多个线程执行文本处理时,就是一个Unix管道(例如:grep | sort)。

在特定示例中,管道流允许使用HttpClient提供的现有RequestEntity实现类。我认为更好的解决方案是创建一个新的实现类,如下所示,因为该示例最终是一个顺序操作,无法从并发实现的复杂性和开销中受益。虽然我将RequestEntity显示为匿名类,但可重用性表明它应该是一流的类。

post.setRequestEntity(new RequestEntity()
{
    public long getContentLength()
    {
        return 0-1;
    }

    public String getContentType()
    {
        return "text/xml";
    }

    public boolean isRepeatable()
    {
        return false;
    }

    public void writeRequest(OutputStream out) throws IOException
    {
        output.setByteStream(out);
        serializer.write(doc, output);
    }
});

答案 2 :(得分:6)

我最近才发现了PipedInputStream / PipedOutputStream类。

我正在开发一个需要通过SSH在远程服务器上执行命令的Eclipse插件。我正在使用JSch,并且Channel API从输入流中读取并写入输出流。但我需要通过输入流提供命令并从输出流中读取响应。这就是PipedInput / OutputStream的用武之地。

import java.io.PipedInputStream;
import java.io.PipedOutputStream;

import com.jcraft.jsch.Channel;

Channel channel;
PipedInputStream channelInputStream = new PipedInputStream();
PipedOutputStream channelOutputStream = new PipedOutputStream();

channel.setInputStream(new PipedInputStream(this.channelOutputStream));
channel.setOutputStream(new PipedOutputStream(this.channelInputStream));
channel.connect();

// Write to channelInputStream
// Read from channelInputStream

channel.disconnect();

答案 3 :(得分:4)

此外,回到原始示例:不,它也没有完全最小化内存使用量。 DOM树被构建,内存中缓冲完成 - 虽然这比完整字节数组副本更好,但它并没有那么好。 但在这种情况下缓冲会更慢;并且还创建了一个额外的线程 - 您不能在单个线程中使用PipedInput / OutputStream对。

PipedXxxStreams有时会很有用,但之所以没有使用它们是因为它们通常不是正确的解决方案。它们可以用于线程间通信,这就是我用它们的价值所在。考虑到SOA如何将大多数此类边界推送到服务之间而不是线程之间,只是没有那么多用例。

答案 4 :(得分:2)

我尝试使用这些类一段时间后,我忘记了细节。但我确实发现他们的实施存在致命缺陷。我不记得它是什么,但我有一个偷偷摸摸的记忆,它可能是一个竞争条件,这意味着他们偶尔会陷入僵局(是的,我当然是在单独的线程中使用它们:它们根本不可用于单线程,并没有设计为)。

我可能会查看他们的源代码,看看我是否可以看到问题所在。

答案 5 :(得分:2)

这是管道有意义的用例:

假设您有第三方lib,例如xslt mapper或crypto lib,它具有如下界面:doSomething(inputStream,outputStream)。并且您不希望在通过电线发送之前缓冲结果。 Apache和其他客户端不允许直接访问线路输出流。最接近的是获取输出流 - 在写入标题后的偏移量中 - 在请求实体对象中。但由于这是在引擎盖下,所以仍然不足以将输入流和输出流传递给第三方lib。管道是解决这个问题的好方法。

顺便提一下,我写了一个Apache的HTTP客户端API [PipedApacheClientOutputStream]的反转,它使用Apache Commons HTTP Client 4.3.4为HTTP POST提供了一个OutputStream接口。这是Piped Streams可能有意义的示例。

答案 6 :(得分:1)

java.io管道有太多的上下文切换(每个字节读/写)和他们的java.nio对应要求你有一些NIO背景和正确使用的通道和东西,这是我自己的管道使用阻塞的实现单个生产者/消费者将快速执行和扩展的队列:

import java.io.IOException;
import java.io.OutputStream;
import java.util.concurrent.*;

public class QueueOutputStream extends OutputStream
{
  private static final int DEFAULT_BUFFER_SIZE=1024;
  private static final byte[] END_SIGNAL=new byte[]{};

  private final BlockingQueue<byte[]> queue=new LinkedBlockingDeque<>();
  private final byte[] buffer;

  private boolean closed=false;
  private int count=0;

  public QueueOutputStream()
  {
    this(DEFAULT_BUFFER_SIZE);
  }

  public QueueOutputStream(final int bufferSize)
  {
    if(bufferSize<=0){
      throw new IllegalArgumentException("Buffer size <= 0");
    }
    this.buffer=new byte[bufferSize];
  }

  private synchronized void flushBuffer()
  {
    if(count>0){
      final byte[] copy=new byte[count];
      System.arraycopy(buffer,0,copy,0,count);
      queue.offer(copy);
      count=0;
    }
  }

  @Override
  public synchronized void write(final int b) throws IOException
  {
    if(closed){
      throw new IllegalStateException("Stream is closed");
    }
    if(count>=buffer.length){
      flushBuffer();
    }
    buffer[count++]=(byte)b;
  }

  @Override
  public synchronized void write(final byte[] b, final int off, final int len) throws IOException
  {
    super.write(b,off,len);
  }

  @Override
  public synchronized void close() throws IOException
  {
    flushBuffer();
    queue.offer(END_SIGNAL);
    closed=true;
  }

  public Future<Void> asyncSendToOutputStream(final ExecutorService executor, final OutputStream outputStream)
  {
    return executor.submit(
            new Callable<Void>()
            {
              @Override
              public Void call() throws Exception
              {
                try{
                  byte[] buffer=queue.take();
                  while(buffer!=END_SIGNAL){
                    outputStream.write(buffer);
                    buffer=queue.take();
                  }
                  outputStream.flush();
                } catch(Exception e){
                  close();
                  throw e;
                } finally{
                  outputStream.close();
                }
                return null;
              }
            }
    );
  }

答案 7 :(得分:0)

  

那么,这个成语有什么问题?如果   这个成语没有错,   为什么我没有看到它?

     

编辑:澄清,PipedInputStream和   PipedOutputStream取代了   样板缓冲区缓冲区复制   到处都是,他们也是   允许您处理传入的数据   同时写出来   处理过的数据他们不使用操作系统   管道

你已经陈述了它的作用,但没有说明你为什么要这样做。

如果您认为这会减少使用的资源(CPU /内存)或提高性能,那么它也不会这样做。但是它会使你的代码更复杂。

基本上你有一个没有问题的解决方案。

答案 8 :(得分:0)

每当

PipedInputStream和PipeOutputStream阻塞等待另一端从已满或空缓冲区读取或写入数据时,它们将在 1秒内休眠其线程。不要使用。