然而,整个Java zip库都被同步到了令人难以忍受的程度,毫无疑问,这是因为它们都是抽象的,用于读/写等,而不是具有良好效率的非同步只读代码。
我已查看了第三方Java库,所有这些库都是巨大的VFS库,比使用大象枪射击苍蝇还要糟糕,或者之所以具有性能优势的唯一原因是它们多线程到大多数线程被阻止在磁盘IO上。
我想做的就是将zip文件拉入byte[],fork一些线程并对其进行处理。没有任何理由需要任何同步方式,因为我单独在内存中使用每个未压缩的文件,没有任何交互。
为什么这必须如此困难?
仅为了记录,经过一些来回的测试,我最终使用的答案如下(完全从头开始,在 while (true)
循环中关闭文件):
使用 DataInputStream.readFully
将整个(在本例中为 50MB)zip 文件读入到一个 byte[]
中。
生成工作线程(每个物理 CPU 核心一个线程,在我的情况下为 4 个),每个线程都取出那个 byte[]
并创建一个 ZipInputStream(ByteArrayInputStream)
。第一个工作线程跳过 0 个条目,第二个跳过 1 个,第三个跳过 2 个,以此类推,因此它们之间都有一个偏移量。工作线程不进行任何同步,因此它们都有自己的 zip 文件元数据等本地副本。这是线程安全的,因为 zip 文件是只读的,工作线程不共享解压缩数据。
每个工作线程读取一个条目并处理它,然后跳过足够多的条目,使它们再次相互偏移一个。因此,第一个线程读取 0、4、8... 条目,第二个线程读取 1、5、9... 条目,依此类推。
所有工作线程都使用 .join() 汇总回来。
我的时间如下:
仅将 zip 文件读入 byte[]
中,没有解压缩(只是 IO),每次迭代平均需要 0.1 秒。
直接在底层文件上使用普通的 ZipFile,会产生一个初始峰值为 0.5 秒,其后每次迭代平均需要 0.26 秒(从关闭前一个 ZipFile 开始重新开始)。
将 ZipFile 读入到一个 byte[]
中,在不进行任何多线程处理的情况下使用 ZipInputStream(ByteArrayInputStream)
创建它,结果产生一个初始峰值为 0.3 秒,其后每次迭代平均需要 0.26 秒,显示磁盘缓存正在发挥作用,使随机访问和初始读取等效。
将 ZipFile 读入到一个 byte[]
中,使用上述描述的 4 个工作线程,并等待它们完成,将时间降低到每次迭代平均需要 0.1 秒。
因此,结论是,通过这种方法,我已经成功地将处理中等大小的 zip 文件与适度强大的计算机的时间降至仅需读取文件所需的时间,而额外的解压缩步骤则完全不可见。显然,在具有数万个条目的巨大 zip 文件上使用相同的方法仍将产生巨大的加速。
看起来我并没有试图优化什么,因为我已经将处理时间减少到了我的样本文件(大约是我需要处理的最大文件的大小)的38%,而这是一个简单单线程方法的时间。
考虑到这个“hack-job”如此出色,想象一下使用专门设计进行此操作的本机Java zip-reader类所可能实现的速度提升。
FileChannel channel = FileChannel.open(Paths.get("/path/to/zip"),
StandardOpenOption.READ);
MappedByteBuffer buffer = channel.map(MapMode.READ_ONLY, 0, channel.size());
buffer
包含了您整个文件的内存映射区域。您可以随心所欲地使用它,例如将offset
和length
传递给线程。我不知道哪个zip库支持这样的操作,但显然您已经有了类似的东西。ZipInputStream
读取它平均只需要不到200毫秒 - 我认为您试图优化的几乎没有什么意义。ZipInputStream
。请参见:https://dev59.com/OW855IYBdhLWcg3wc0Dy#6603018 - Thomas JungblutZipInputStream
下面的BufferedInputStream
需要一些时间(200ms vs. 270ms;600ms未缓冲)。对于他的多线程计划,顺序InputStream
显然是无用的。顺便说一下,我仍然不知道他想要什么样的并行性:每个文件一个线程? - Thomas Jungblut正如您所注意到的,ZipFile
中的所有方法都是同步的。但这仅阻止了在磁盘上打开的相同确切zipfile的不同 ZipFile
实例之间同时运行的多个线程。
如果您希望多个线程以可扩展的方式从同一zipfile中读取,则必须为每个线程打开一个 ZipFile
实例。这样,ZipFile
方法中的每个线程锁定不会阻止除一个线程外的所有线程同时从zipfile中读取。这也意味着当每个线程完成读取后关闭 ZipFile
时,它们关闭自己的实例而不是共享实例,因此您不会在第二个及随后的关闭时出现异常。