关于设计模式:为什么没有更多的Java代码使用PipedInputStream / PipedOutputStream?

Why doesn't more Java code use PipedInputStream / PipedOutputStream?

我最近发现了这个成语,我想知道我是否遗漏了一些东西。我从没见过它用过。我在野外工作的几乎所有Java代码都支持将数据拖到字符串或缓冲区中,而不是像这个例子(例如使用HTTPclipse和XML API):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
    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加载/保存API将XML文档序列化为HTTP请求的内容。据我所知,它将内存的使用降到最低,只需要很少的额外代码(仅限于用于RunnablePipedInputStreamPipedOutputStream的几行)。

那么,这个成语怎么了?如果这个成语没什么问题,为什么我没看过呢?

编辑:为了澄清,PipedInputStreamPipedOutputStream将样板缓冲区替换为到处显示的缓冲区副本,它们还允许您同时处理传入的数据和写出处理的数据。他们不使用操作系统管道。


来自javadocs:

Typically, data is read from a PipedInputStream object by one thread and data is written to the corresponding PipedOutputStream by some other thread. Attempting to use both objects from a single thread is not recommended, as it may deadlock the thread.

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

我假设另一个原因是许多开发人员不理解它的目的/好处。


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

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

要将一些评论(至少我对它们的看法)纳入主要回应:

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

在特定示例中,管道流允许使用httpclient提供的现有requestEntity实现类。我认为一个更好的解决方案是创建一个新的实现类,如下所示,因为该示例最终是一个顺序操作,不能从并发实现的复杂性和开销中获益。当我将requestEntity显示为匿名类时,可重用性将指示它应该是一个一流的类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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);
    }
});


我最近也只发现了pipedinputstream/pipedOutputstream类。

我正在开发一个Eclipse插件,它需要通过ssh在远程服务器上执行命令。我使用的是JSCH,通道API从输入流读取数据并写入输出流。但我需要通过输入流输入命令并从输出流读取响应。这就是pipedinput/outputstream进入的地方。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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();


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

有时候pipedxxx流是有用的,但是它们没有被更多地使用的原因是它们通常不是正确的解决方案。它们对于线程间的通信是可以的,这就是我使用它们的价值所在。考虑到SOA是如何将大多数这样的边界推到服务之间,而不是线程之间的,这只是因为没有那么多的用例。


下面是一个管道有意义的用例:

假设您有一个第三方库,例如具有如下接口的XSLT映射器或加密库:dosomething(inputstream、outputstream)。你不想在发送之前缓冲结果。Apache和其他客户机不允许直接访问线输出流。最接近的方法是在请求实体对象中写入头之后,在偏移量处获取输出流。但由于这是在幕后进行的,所以还不足以将输入流和输出流传递给第三方库。管道是解决这个问题的好办法。

顺便说一句,我写了一个Apache的HTTP客户机API[PipedApacheClientOutputstream]的反转,它使用Apache Commons HTTP客户机4.3.4为HTTP Post提供了一个输出流接口。这是一个管道流可能有意义的例子。


我曾经尝试过用这些类来做一些事情,我忘记了细节。但我发现它们的实现存在致命的缺陷。我不记得它是什么,但我有一个潜移默化的记忆,它可能是一个竞争条件,这意味着它们偶尔会死锁(是的,当然我是在单独的线程中使用它们:它们在单个线程中根本不可用,而且不是设计为可用的)。

我可能会看看他们的源代码,看看我是否能看到问题可能是什么。


java.io管道有太多的上下文切换(每字节读/写),它们的java.nio对应项要求您具有一些nio背景和通道和内容的正确使用,这是我自己使用阻塞队列实现的管道,对于单个生产商/消费者来说,它将执行得很快并具有很好的伸缩性:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
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;
              }
            }
    );
  }

So, what's wrong with this idiom? If
there's nothing wrong with this idiom,
why haven't I seen it?

EDIT: to clarify, PipedInputStream and
PipedOutputStream replace the
boilerplate buffer-by-buffer copy that
shows up everywhere, and they also
allow you to process incoming data
concurrently with writing out the
processed data. They don't use OS
pipes.

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

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

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