作者:不学无数的程序员  
来源:https://juejin.im/post/5d5626cdf265da03a65312be

有一个需求需要将前端传过来的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

第一次优化过程-从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

优化过后的代码如下

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

输出

------Buffer
fileSize:20M
consum time:1808

可以看到相比较于第一次使用FileInputStream效率已经提升了许多了

第二次优化过程-从2秒到1秒

使用缓冲区buffer的话已经是满足了我的需求了,但是秉着学以致用的想法,就想着用NIO中知识进行优化一下。

使用Channel

为什么要用Channel呢?因为在NIO中新出了Channel和ByteBuffer。正是因为它们的结构更加符合操作系统执行I/O的方式,所以其速度相比较于传统IO而言速度有了显著的提高。Channel就像一个包含着煤矿的矿藏,而ByteBuffer则是派送到矿藏的卡车。也就是说我们与数据的交互都是与ByteBuffer的交互。

在NIO中能够产生FileChannel的有三个类。

分别是FileInputStream、FileOutputStream、以及既能读又能写的RandomAccessFile。

源码如下

public static void zipFileChannel() { //开始时间  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++) {  try (FileChannel fileChannel = new FileInputStream(JPG_FILE).getChannel()) {  zipOut.putNextEntry(new ZipEntry(i + SUFFIX_FILE));  fileChannel.transferTo(0, FILE_SIZE, writableByteChannel);  }  }  printInfo(beginTime);  } catch (Exception e) {  e.printStackTrace();  }
}

我们可以看到这里并没有使用ByteBuffer进行数据传输,而是使用了transferTo的方法。这个方法是将两个通道进行直连。

This method is potentially much more efficient than a simple loop
* that reads from this channel and writes to the target channel. Many
* operating systems can transfer bytes directly from the filesystem cache
* to the target channel without actually copying them.

这是源码上的描述文字,大概意思就是使用transferTo的效率比循环一个Channel读取出来然后再循环写入另一个Channel好。操作系统能够直接传输字节从文件系统缓存到目标的Channel中,而不需要实际的copy阶段

copy阶段就是从内核空间转到用户空间的一个过程

可以看到速度相比较使用缓冲区已经有了一些的提高。

------Channel
fileSize:20M
consum time:1416

内核空间和用户空间

那么为什么从内核空间转向用户空间这段过程会慢呢?首先我们需了解的是什么是内核空间和用户空间。在常用的操作系统中为了保护系统中的核心资源,于是将系统设计为四个区域,越往里权限越大,所以Ring0被称之为内核空间,用来访问一些关键性的资源。Ring3被称之为用户空间。

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

那么我们如果此时应用程序(应用程序是都属于用户态的)需要访问核心资源怎么办呢?那就需要调用内核中所暴露出的接口用以调用,称之为系统调用。例如此时我们应用程序需要访问磁盘上的文件。

此时应用程序就会调用系统调用的接口open方法,然后内核去访问磁盘中的文件,将文件内容返回给应用程序。大致的流程如下

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

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

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

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

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

  1. 不安全

  2. 消耗更多,因为它不是在JVM中直接开辟空间。这部分内存的回收只能依赖于垃圾回收机制,垃圾什么时候回收不受我们控制。

  3. 数据写入物理内存缓冲区中,程序就丧失了对这些数据的管理,即什么时候这些数据被最终写入从磁盘只能由操作系统来决定,应用程序无法再干涉。

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

使用内存映射文件

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的速度差不多的

使用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

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

//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();  }
}

总结

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

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

不是我吹,20M的压缩文件我只用了1秒!相关推荐

  1. mysql 上传rar文件大小_js文件上传 自定义压缩文件和文件格式及大小限制

    1 $(function() {2 var count = 0;3 /*----------------------------------------------文件上传-------------- ...

  2. 云计算之路-试用Azure:数据库备份压缩文件在虚拟机上的恢复速度测试

    测试环境:Windows Azure上海机房,虚拟机配置为大型(四核,7 GB 内存),磁盘情况见下图. 数据库备份压缩文件大于为12.0 GB (12,914,327,552 bytes),放置于T ...

  3. java压缩文件耗时:30秒到1秒的优化过程

    点击上方蓝字关注我们 有一个需求需要将前端传过来的10张照片,然后后端进行处理以后压缩成一个压缩包通过网络流传输出去.之前没有接触过用Java压缩文件的,所以就直接上网找了一个例子改了一下用了,改完以 ...

  4. python 图像压缩后前端解压_Python在后台自动解压各种压缩文件的实现方法

    1.需求描述 编写一个 Python 程序,每次下载压缩包形式的文件后,自动将内部文件解压到当前文件夹后将压缩包删除,通过本案例可以学到的知识点: os 模块综合应用 glob 模块综合应用 利用 g ...

  5. linux 压缩文件夹格式,Linux下常见文件格式的压缩、解压小结

    Linux下常见文件格式的压缩.解压小结 .tar 解包: tar xvf FileName.tar 打包:tar cvf FileName.tar DirName (注:tar是打包,不是压缩!) ...

  6. linux下解压缩文件中文乱码问题的解决

    在windows上压缩的文件,是以系统默认编码中文来压缩文件.由于zip文件中没有声明其编码,所以linux上的unzip一般以默认编码解压,中文文件名会出现乱码. 虽然2005年就有人把这报告为bu ...

  7. 如何破解压缩文件密码-省时省力的方法

    压缩文件破解工具下载地址:http://www.cnblogs.com/spring_wang/archive/2013/06/14/3135163.html 应该很多人都碰到过RAR加密.解密的问题 ...

  8. asp在线压缩和解压缩文件(文件夹)

    asp在线压缩和解压缩文件(文件夹) <% '\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\ '\\ '\\ 1. c:\ ...

  9. 一个撇脚的java压缩文件工具类

    今天弄里一个压缩文件的工具类,功能不是很完善,只支持压缩后单级目录,二级文件夹的文件名好像只能用中文,并且解压后还是乱码.请各位高手大侠批评指教,不胜感激! package tests; import ...

最新文章

  1. linux qt 生成word,Qt 生成word、pdf文檔
  2. Git 创建两个“本地分支”协同工作
  3. 【struts2+spring+hibernate】ssh框架整合开发
  4. 解决hao123胁持chrome等浏览器主页问题
  5. Castle学习之一:安装与环境设置
  6. SpringBoot2 整合JTA组件,多数据源事务管理
  7. DPDK 跟踪库tracepoint源码实例分析
  8. 解决Ubuntu 18.04中文输入法的问题
  9. 纠正口呼吸,信医生别信“神器”
  10. Python3的对象和类
  11. html大一期末考试,2017年大学英语大一期末考试试题及答案
  12. 高斯平滑算法 c语言,高斯模糊算法(gaussian)
  13. 《NFL橄榄球》:田纳西泰坦·橄榄1号位
  14. 计算机教改论文发在那家核心期刊,教改类文章容易发的核心期刊有哪些
  15. linux-tomcat下载安装(最新)
  16. 专题论坛:云计算平台构建与实践论坛
  17. 页面布局的几种宽度设置方式—html
  18. kubelet源码分析(二)之 NewMainKubelet
  19. 小霸王推出可挖矿的VR眼镜,把IPFS、以太坊技术全用上了
  20. dnf服务器炸团门票怎么找回,DNF:还在为魔界裂缝门票发愁吗?这些地方可以轻松获取门票...

热门文章

  1. Stanford UFLDL教程 Softmax回归
  2. Day 21:Docker 入门教程
  3. for、while循环及其变体
  4. #65279导致页面顶部空白一行解决方法
  5. 使用 Fluentd 和 ElasticSearch Stack 实现 Kubernetes 的集群 Logging
  6. 解决服务器上安装不了centos7.4的问题
  7. 130242014076+陈旭+第3次实验
  8. windows下ngnix+php配置
  9. SQL_由创建表引出
  10. POJ 1556 The Doors (未完)