快捷搜索:  汽车  科技

怎么把1g文件压缩到100m(压缩20M文件从30秒到1秒)

怎么把1g文件压缩到100m(压缩20M文件从30秒到1秒)/** * Reads a byte of data from this input stream. This method blocks * if no input is yet available. * * @return the next byte of data or <code>-1</code> if the end of the * file is reached. * @exception IOException if an I/O error occurs. */ public native int read() throws IOException; 这是一个调用本地方法与原生操作系统进行交互,从磁盘中读取数据。每读取一个字节的数据就调用一次本地方法与操作系统交互,是非常耗时的。例如我们现在有30000个字节的数据,如果使用Fil

* 推荐阅读*

*我说精通字符串,面试官竟然问我Java中的String有没有长度限制?

*国内Java面试总是问StringBuffer?档次为什么这么低?

*Java 面试,如何坐等 offer?

*秋招Java岗:最全BAT大厂面试题整理

01 前言

有一个需求需要将前端传过来的10张照片,然后后端进行处理以后压缩成一个压缩包通过网络流传输出去。之前没有接触过用Java压缩文件的,所以就直接上网找了一个例子改了一下用了,改完以后也能使用,但是随着前端所传图片的大小越来越大的时候,耗费的时间也在急剧增加,最后测了一下压缩20M的文件竟然需要30秒的时间。压缩文件的代码如下。

public static void zipFileNoBuffer() { File zipFile = new File(ZIP_FILE); try (ZipOutputStream zipOut = new ZipOutputStream(new FileOutputStream(zipFile))) { //开始时间 long beginTime = System.currentTimeMillis(); for (int i = 0; i < 10; i ) { try (InputStream input = new FileInputStream(JPG_FILE)) { zipOut.putNextEntry(new ZipEntry(FILE_NAME i)); int temp = 0; while ((temp = input.read()) != -1) { zipOut.write(temp); } } } printInfo(beginTime); } catch (Exception e) { e.printStackTrace(); } }

这里找了一张2M大小的图片,并且循环十次进行测试。打印的结果如下,时间大概是30秒。

fileSize:20M consum time:29599 02 第一次优化过程-从30秒到2秒

进行优化首先想到的是利用缓冲区BufferInputStream。在FileInputStream中read()方法每次只读取一个字节。源码中也有说明。

/** * Reads a byte of data from this input stream. This method blocks * if no input is yet available. * * @return the next byte of data or <code>-1</code> if the end of the * file is reached. * @exception IOException if an I/O error occurs. */ public native int read() throws IOException;

这是一个调用本地方法与原生操作系统进行交互,从磁盘中读取数据。每读取一个字节的数据就调用一次本地方法与操作系统交互,是非常耗时的。例如我们现在有30000个字节的数据,如果使用FileInputStream那么就需要调用30000次的本地方法来获取这些数据,而如果使用缓冲区的话(这里假设初始的缓冲区大小足够放下30000字节的数据)那么只需要调用一次就行。因为缓冲区在第一次调用read()方法的时候会直接从磁盘中将数据直接读取到内存中。随后再一个字节一个字节的慢慢返回。

BufferedInputStream内部封装了一个byte数组用于存放数据,默认大小是8192

优化过后的代码如下

用户态、内核态:线程处于内核空间称之为内核态,线程处于用户空间属于用户态

那么我们如果此时应用程序(应用程序是都属于用户态的)需要访问核心资源怎么办呢?那就需要调用内核中所暴露出的接口用以调用,称之为系统调用。例如此时我们应用程序需要访问磁盘上的文件。此时应用程序就会调用系统调用的接口open方法,然后内核去访问磁盘中的文件,将文件内容返回给应用程序。大致的流程如下

怎么把1g文件压缩到100m(压缩20M文件从30秒到1秒)(1)

直接缓冲区和非直接缓冲区

既然我们要读取一个磁盘的文件,要废这么大的周折。有没有什么简单的方法能够使我们的应用直接操作磁盘文件,不需要内核进行中转呢?有,那就是建立直接缓冲区了。

  • 非直接缓冲区:非直接缓冲区就是我们上面所讲内核态作为中间人,每次都需要内核在中间作为中转。

怎么把1g文件压缩到100m(压缩20M文件从30秒到1秒)(2)

  • 直接缓冲区:直接缓冲区不需要内核空间作为中转copy数据,而是直接在物理内存申请一块空间,这块空间映射到内核地址空间和用户地址空间,应用程序与磁盘之间数据的存取通过这块直接申请的物理内存进行交互。

怎么把1g文件压缩到100m(压缩20M文件从30秒到1秒)(3)

既然直接缓冲区那么快,我们为什么不都用直接缓冲区呢?其实直接缓冲区有以下的缺点。直接缓冲区的缺点:

  • 不安全
  • 消耗更多,因为它不是在JVM中直接开辟空间。这部分内存的回收只能依赖于垃圾回收机制,垃圾什么时候回收不受我们控制。
  • 数据写入物理内存缓冲区中,程序就丧失了对这些数据的管理,即什么时候这些数据被最终写入从磁盘只能由操作系统来决定,应用程序无法再干涉。

综上所述,所以我们使用transferTo方法就是直接开辟了一段直接缓冲区。所以性能相比而言提高了许多

3.2 使用内存映射文件

NIO中新出的另一个特性就是内存映射文件,内存映射文件为什么速度快呢?其实原因和上面所讲的一样,也是在内存中开辟了一段直接缓冲区。与数据直接作交互。源码如下

//Version 4 使用Map映射文件 public static void zipFileMap() { //开始时间 long beginTime = System.currentTimeMillis(); File zipFile = new File(ZIP_FILE); try (ZipOutputStream zipOut = new ZipOutputStream(new FileOutputStream(zipFile)); WritableByteChannel writableByteChannel = Channels.newChannel(zipOut)) { for (int i = 0; i < 10; i ) { zipOut.putNextEntry(new ZipEntry(i SUFFIX_FILE)); //内存中的映射文件 MappedByteBuffer mappedByteBuffer = new RandomAccessFile(JPG_FILE_PATH "r").getChannel() .map(FileChannel.MapMode.READ_ONLY 0 FILE_SIZE); writableByteChannel.write(mappedByteBuffer); } printInfo(beginTime); } catch (Exception e) { e.printStackTrace(); } }

打印如下

---------Map fileSize:20M consum time:1305

可以看到速度和使用Channel的速度差不多的。

3.3 使用Pipe

Java NIO 管道是2个线程之间的单向数据连接。Pipe有一个source通道和一个sink通道。其中source通道用于读取数据,sink通道用于写入数据。可以看到源码中的介绍,大概意思就是写入线程会阻塞至有读线程从通道中读取数据。如果没有数据可读,读线程也会阻塞至写线程写入数据。直至通道关闭。

Whether or not a thread writing bytes to a pipe will block until another thread reads those bytes

怎么把1g文件压缩到100m(压缩20M文件从30秒到1秒)(4)

我想要的效果是这样的。源码如下

//Version 5 使用Pip public static void zipFilePip() { long beginTime = System.currentTimeMillis(); try(WritableByteChannel out = Channels.newChannel(new FileOutputStream(ZIP_FILE))) { Pipe pipe = Pipe.open(); //异步任务 CompletableFuture.runAsync(()->runTask(pipe)); //获取读通道 ReadableByteChannel readableByteChannel = pipe.source(); ByteBuffer buffer = ByteBuffer.allocate(((int) FILE_SIZE)*10); while (readableByteChannel.read(buffer)>= 0) { buffer.flip(); out.write(buffer); buffer.clear(); } }catch (Exception e){ e.printStackTrace(); } printInfo(beginTime); } //异步任务 public static void runTask(Pipe pipe) { try(ZipOutputStream zos = new ZipOutputStream(Channels.newOutputStream(pipe.sink())); WritableByteChannel out = Channels.newChannel(zos)) { System.out.println("Begin"); for (int i = 0; i < 10; i ) { zos.putNextEntry(new ZipEntry(i SUFFIX_FILE)); FileChannel jpgChannel = new FileInputStream(new File(JPG_FILE_PATH)).getChannel(); jpgChannel.transferTo(0 FILE_SIZE out); jpgChannel.close(); } }catch (Exception e){ e.printStackTrace(); } } 04 总结

生活处处都需要学习,有时候只是一个简单的优化,可以让你深入学习到各种不同的知识。所以在学习中要不求甚解,不仅要知道这个知识也要了解为什么要这么做。

知行合一:学习完一个知识要尽量应用一遍。这样才能记得牢靠。

如果觉得有所收获的话可以点赞关注我和转发分享给更多的程序猿们哦~

怎么把1g文件压缩到100m(压缩20M文件从30秒到1秒)(5)

猜您喜欢: