如何在最短的时间内克隆Java中的InputStream

6
有人能告诉我如何尽量少地创建时间来克隆输入流吗? 我需要为多个方法克隆一个输入流以处理该IS。 我已经尝试过三种方法,但由于某种原因,事情都不起作用。

方法#1:感谢stackoverflow社区,我找到了以下链接并已将代码片段合并到我的程序中。

如何克隆InputStream?

然而,使用这段代码可能需要长达一分钟的时间(对于一个10MB文件)来创建克隆的输入流,并且我的程序需要尽可能快。

    int read = 0;
    byte[] bytes = new byte[1024*1024*2];

    ByteArrayOutputStream bos = new ByteArrayOutputStream();
    while ((read = is.read(bytes)) != -1)
        bos.write(bytes,0,read);
    byte[] ba = bos.toByteArray();

    InputStream is1 = new ByteArrayInputStream(ba);
    InputStream is2 = new ByteArrayInputStream(ba);
    InputStream is3 = new ByteArrayInputStream(ba);

方法二: 我还尝试使用BufferedInputStream来克隆IS。这个方法很快(最慢的创建时间为1毫秒,最快的为0毫秒)。然而,在我发送is1进行处理后,处理is2和is3的方法抛出了一个错误,说没有要处理的内容,就像下面的所有3个变量都引用了同一个IS一样。
    is = getFileFromBucket(path,filename);
    ...
    ...
    InputStream is1 = new BufferedInputStream(is);
    InputStream is2 = new BufferedInputStream(is);
    InputStream is3 = new BufferedInputStream(is);

方法三: 我认为编译器在欺骗我。我检查了is1的markSupported()函数,对于上述两个示例,它返回true,所以我认为我可以运行。

    is1.mark() 
    is1.reset()

或者只需
    is1.reset();

在传递IS到我的各个方法之前。在上述两个示例中,我收到一个错误,说它是无效的标记。
我现在已经没有想法了,所以提前感谢您能给我的任何帮助。
附言:从我收到的评论中,需要澄清一些关于我的情况的事情: 1)此程序正在虚拟机上运行 2)输入流由另一个方法传递给我。我不从本地文件读取 3)输入流的大小未知

3
在我的电脑上,对于一个10 MB的文件,运行方法1的代码需要18毫秒。你的硬件有问题吗? - F.J
谢谢回复。我认为我的硬件没有问题。我突然想起来忘了提两件事:a)这是在虚拟机上进行的,b)输入流是一个jpg文件。最快用了11秒,但根据我的测试,平均需要30秒左右,最慢的是大约1分钟(确切地说是53秒)。 - Classified
1
如果你这样做,可能会得到一些小的提升: byte[] ba = new byte[is.available()]; // 如果是 FileInputStream,这个方法有效 new DataInputStream(is).readFully(ba); - F.J
2
@FJ - 对我来说,一个10Mb的文件只需要18ms,这意味着整个文件必须已经在操作系统的内存缓存中了。 - Stephen C
@StephenC - 很好的观点,很容易被忘记。 - F.J
4个回答

6
如何尽可能少地创建并克隆输入流?我需要将输入流克隆多次以便多个方法处理该输入流。
您可以创建一种自定义的 ReusableInputStream 类,在其中立即对第一次完整读取时的内部 ByteArrayOutputStream 进行写入,然后在读取最后一个字节时将其包装在 ByteBuffer 中,并在后续完整读取中重复使用同一个 ByteBuffer,当达到限制时会自动翻转。这样可以避免第一次尝试中的一次完整读取。
以下是基本示例:
public class ReusableInputStream extends InputStream {

    private InputStream input;
    private ByteArrayOutputStream output;
    private ByteBuffer buffer;

    public ReusableInputStream(InputStream input) throws IOException {
        this.input = input;
        this.output = new ByteArrayOutputStream(input.available()); // Note: it's resizable anyway.
    }

    @Override
    public int read() throws IOException {
        byte[] b = new byte[1];
        read(b, 0, 1);
        return b[0];
    }

    @Override
    public int read(byte[] bytes) throws IOException {
        return read(bytes, 0, bytes.length);
    }

    @Override
    public int read(byte[] bytes, int offset, int length) throws IOException {
        if (buffer == null) {
            int read = input.read(bytes, offset, length);

            if (read <= 0) {
                input.close();
                input = null;
                buffer = ByteBuffer.wrap(output.toByteArray());
                output = null;
                return -1;
            } else {
                output.write(bytes, offset, read);
                return read;
            }
        } else {
            int read = Math.min(length, buffer.remaining());

            if (read <= 0) {
                buffer.flip();
                return -1;
            } else {
                buffer.get(bytes, offset, read);
                return read;
            }
        }

    }

    // You might want to @Override flush(), close(), etc to delegate to input.
}

请注意,实际的任务是在int read(byte[], int, int)中执行,而不是在int read()中执行,因此当调用者本身也使用byte[]缓冲区进行流传输时,预计速度会更快。
您可以按以下方式使用它:
InputStream input = new ReusableInputStream(getFileFromBucket(path,filename));
IOUtils.copy(input, new FileOutputStream("/copy1.ext"));
IOUtils.copy(input, new FileOutputStream("/copy2.ext"));
IOUtils.copy(input, new FileOutputStream("/copy3.ext"));

关于性能问题,每10MB 1分钟的情况更可能是硬件问题,而非软件问题。我的7200转每分钟的笔记本硬盘只需要不到1秒钟就可以完成。

感谢提供代码片段。我会尝试它以及其他建议! - Classified

3
然而,使用此代码会花费长达一分钟(对于一个10MB的文件)来创建克隆输入流,而我的程序需要尽可能快。拷贝流需要时间,这通常是克隆流的唯一方法。除非缩小问题范围,否则性能很难得到显著提升。以下是几种可能改进性能的情况:
- 如果您事先知道流中的字节数,则可以直接读取到最终的字节数组中。 - 如果您知道数据来自文件,则可以为文件创建内存映射缓冲区。
但是,将大量字节移动需要时间。使用您问题中的代码,一个10Mb的文件花费1分钟的时间,这表明真正的瓶颈根本不在Java中。

2
关于你的第一种方法,即将所有字节放入ByteArrayOutputStream中:
  • 首先,这种方法会消耗大量内存。如果您没有确保JVM分配了足够的内存,它将需要在处理流时动态请求内存,这会耗费时间。
  • 你的ByteArrayOutputStream最初创建了一个32字节的缓冲区。每次你尝试把东西放进去,如果它不适合现有的字节数组中,就会创建一个新的更大的数组,并将旧的字节复制到新的数组中。由于你每次都使用2MB的输入,所以你强制ByteArrayOutputStream反复将其数据复制到更大的数组中,每次增加2MB的数组大小。
  • 由于旧数组是垃圾,很可能它们的内存正在被垃圾收集器回收,这使得你的复制过程变得更慢。
  • 也许你应该使用指定初始缓冲区大小的构造函数来定义ByArrayOutputStream。设置大小越准确,处理速度应该越快,因为需要较少的中间复制。

你的第二种方法是错误的,你不能在不同的其他流中装饰相同的输入流并期望它们能正常工作。当字节被一个流消耗时,内部流也会被耗尽,不能为其他流提供准确的数据。

在我扩展我的答案之前,让我问一下,你的其他方法是否期望在单独的线程上运行输入流的副本?因为如果是这样,这听起来像是PipedOutputStream和PipedInputStream的工作?


感谢您的回复。由于另一种方法是将输入流传递给我,我不知道输入流的大小。我尝试将字节数组设置为8MB,但仍然需要很长时间。有人建议我使用BufferedInputStream,我想我没有正确使用它,所以我的错误。我计划在其他方法中使用线程,所以我会研究您提出的PipedIS和PipedOS,看看它是否有帮助。现在,我只是想让所有东西按顺序工作,然后再开始使用线程。 - Classified

1

您是打算让这些方法并行运行还是按顺序运行?如果是按顺序,我认为没有必要克隆输入流,所以我得假设您计划开启线程来管理每个流。

我现在不在电脑旁边无法测试,但我认为最好的方法是分块读取输入,比如每次1024字节,然后将这些块(或块的数组副本)推送到输出流上,并将输入流附加到它们的线程末端。如果没有数据可用,请让读取器阻塞等待。


感谢您的回复和建议。是的,我计划使用线程……一旦我弄清楚如何解决这个瓶颈问题。我会尝试这样做,分块读取,但我正在从另一个方法中传入输入流,所以我需要看看在我的情况下是否可行。 - Classified

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接