Java中的multithreading解压缩

所以,我试图用Java对zip文件进行只读访问,以multithreading方式解压缩,因为我的标准简单的ZipFile / ZipEntry单线程解决方案使用枚举和输入流而不是什么导致它采取大约五个完整的秒钟,只需要将一个50兆的zipfile解压缩到内存中,这需要一秒钟的AT MOST让我的磁盘无需解压缩即可读取。

然而,整个Java zip库被同步到令人难以置信的令人讨厌的程度,毫无疑问,因为它完全被抽象用于读/写等。 在相同的代码中,而不是具有良好的有效非同步只读代码。

我看过第三方Java库,它们都是大型VFS库,比使用大象枪射击更糟糕,或者它们具有性能优势的唯一原因是它们multithreading到达无论如何,大多数线程都在磁盘IO上阻塞。

我想要做的就是将zipfile拉入byte [],分叉一些线程,然后处理它。 没有任何理由可以以任何方式进行任何同步,因为我在内存中单独使用的每个解压缩文件都没有交互。

为什么一定要这么难?

使用Java来实现这一目标的最快方法是使用NIO。 您可以使用MappedByteBuffer将文件直接映射到内存中。

 FileChannel channel = FileChannel.open(Paths.get("/path/to/zip"), StandardOpenOption.READ); MappedByteBuffer buffer = channel.map(MapMode.READ_ONLY, 0, channel.size()); 

现在buffer包含整个文件的内存映射区域。 你可以用它做任何你想做的事情,例如将offsetlength传递给线程。 我不知道哪个zip lib支持它,但显然你已经有类似的东西了。

仅供参考,我使用50 mb单文件存档进行了一些测试,平均用不到200毫秒的时间用普通的ZipInputStream读取它 – 我认为你在这里尝试优化的几乎没有。

为了后人的缘故,经过一些来回测试后,我最终使用的答案如下(完整的迭代从头开始,在一段while (true)循环中关闭文件):

  • 使用DataInputStream.readFully将整个(在这种情况下为50兆byte[] )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) ,根本没有multithreading,导致0.3秒的初始峰值,之后每次迭代平均0.26秒,表明磁盘缓存是具有渲染随机访问和初始读取等效的效果。

  • 将ZipFile读入byte[] ,如上所述产生具有该byte[] 4个工作线程,并等待它们完成,每次迭代使时间平均减少0.1秒。

因此,通过这种方法判断我已经成功地将中等大小的zip文件的处理与中等强大的计算机一起处理到简单地物理读取文件所需的时间,并且额外的解压缩步骤根本不再明显。 显然,在包含数万个条目的巨大zip文件上使用相同的方法仍然会产生巨大的加速。

似乎我没有尝试优化任何东西,考虑到我将样本文件的处理时间(大约是我需要使用的最大的文件大小)减少到38%的简单单线程方法。

考虑到这个黑客工作的效果非常好,想象一下本地Java zip-reader类可能的加速,实际上是为了在没有内置同步的情况下实现这一目的。

正如您所注意到的, ZipFile中的所有方法都是同步的。 但是这只会阻止多个线程同时在不同的 ZipFile实例上运行,并为磁盘上相同的zipfile打开。

如果您希望多个线程以可伸缩的方式从同一个zip文件中读取,则必须为每个线程打开一个ZipFile实例。 这样, ZipFile方法中的每线程锁定不会阻止除一个线程之外的所有线程一次从zipfile读取。 这也意味着当每个线程在完成读取后关闭ZipFile ,它们会关闭自己的实例,而不是共享实例,因此您不会在第二次和后续关闭时获得exception。

Protip:如果你真的关心速度,你可以通过从第一个ZipFile实例读取所有ZipEntry对象,并与所有线程共享它们来获得更多性能,以避免重复分别为每个线程读取ZipEntry对象的工作。 ZipEntry对象本身并不依赖于特定的ZipFile实例, ZipEntry只记录将与任何ZipFile对象一起ZipFile元数据,该对象表示ZipEntry来自的相同zipfile。