文章来源公众号:IT牧场

有一个需求需要将前端传过来的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。在FileInputStreamread()方法每次只读取一个字节。源码中也有说明。

/**
* 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中新出了ChannelByteBuffer。正是因为它们的结构更加符合操作系统执行I/O的方式,所以其速度相比较于传统IO而言速度有了显著的提高。Channel就像一个包含着煤矿的矿藏,而ByteBuffer则是派送到矿藏的卡车。也就是说我们与数据的交互都是与ByteBuffer的交互。

在NIO中能够产生FileChannel的有三个类。分别是FileInputStreamFileOutputStream、以及既能读又能写的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被称之为用户空间。

image

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

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

image

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

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

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

    image

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

    image

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

  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

image

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

//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文件从30秒到1秒的优化过程的更多相关文章

  1. 读网文《将20M文件从30秒压缩到1秒,我是如何做到的?》做实验

    先在微信公众号上看到网文<将20M文件从30秒压缩到1秒,我是如何做到的?>,然后在网上搜索了一下,看到了原文:https://www.jianshu.com/p/2e46ccb125ef ...

  2. 如何使用grunt压缩js文件

    jQuery在使用grunt,bootstrap在使用grunt,百度UEditor在使用grunt,你没有理由不学.不用! 1. 前言 各位web前端开发人员,如果你现在还不知道grunt或者听说过 ...

  3. 使用VC++压缩解压缩文件夹

    前言   项目中要用到一个压缩解压缩的模块, 看了很多文章和源代码,  都不是很称心, 现在把我自己实现的代码和大家分享. 要求: 1.使用Unicode(支持中文). 2.使用源代码.(不使用静态或 ...

  4. gulp压缩css文件跟js文件

    越到最后啊 就越发现,真的很理解那句话 就是自己多学一点一点知识,就少一句问别人的东西 这是多么痛苦的领悟 今天需要压缩css跟js文件 然后不懂啊 就问别人啊 就问啊问啊 然后再上网了解啊了解啊 用 ...

  5. zlib压缩一个文件为gzip格式

    网上有很多针对zlib的总结,但是很零散,自己经过总结,实现了用zlib压缩一个文件为gzip格式,似的可以直接使用winr工具解压. 具体方法是使用zlib的deflate系列函数,将buffer压 ...

  6. iis7 压缩js文件和启用gzip压缩

    压缩js文件 打开IIS 7的配置文件:c:\windows\system32\inetsrv\config\applicationhost.config 在<staticContent loc ...

  7. [转]C#压缩打包文件

    /// <summary> /// 压缩和解压文件 /// </summary> public class ZipClass { /// <summary> /// ...

  8. albert1017 Linux下压缩某个文件夹(文件夹打包)

    albert1017 Linux下压缩某个文件夹(文件夹打包) tar -zcvf /home/xahot.tar.gz /xahottar -zcvf 打包后生成的文件名全路径 要打包的目录例子:把 ...

  9. node.js 使用 UglifyJS2 高效率压缩 javascript 文件

    UglifyJS2 这个工具使用很长时间了,但之前都是在 gulp 自动构建 时用到了 UglifyJS 算法进行压缩. 最近玩了一下 UglifyJS2 ,做了一个 在线压缩javascript工具 ...

随机推荐

  1. 【Java线程与内存分析工具】VisualVM与MAT简明教程

    目录 前言 VisualVM 安装与配置 本地使用 远程监控 MAT 使用场景 安装与配置 获得堆转储文件 分析堆转储文件 窥探对象内存值 堆转储文件对比分析 总结 前言 本文将简要介绍Java线程与 ...

  2. [转]在.NET Core 2.x中将多个强类型设置实例与命名选项一起使用

    自1.0版之前,ASP.NET Core已使用“ 选项”模式配置强类型设置对象.从那时起,该功能获得了更多功能.例如,引入了ASP.NET Core 1.1 IOptionsSnapshot,它允许您 ...

  3. 英语阅读——Speaking Chinese in America

    这篇文章是<新视野大学英语>第四册的第五单元的文章,第一遍英语阅读完后对比中文,发现自己对作者的观点理解有些出入.作者反对的是认为中国说话客套而美国人直接的观点,利用自己的经历表达了中文也 ...

  4. JVM GC系列 — GC算法

    一.前言 从本篇文章开始,将开始一个新的系列JVM.JVM是一个非常庞大且复制的技术体系,但是对于程序猿的升级,走向更高阶所必要经历的,曾经也下决心要好好学习一番,然而毅力不足都中途放弃. GC的作用 ...

  5. git clone: HTTP Basic: Access denied 错误

    git clone 报 HTTP Basic: Access denied 错误 解决方案: 1. 如果账号密码有变动 用这个命令 git config –-system –-unset creden ...

  6. JAVA 设置模块间的依赖关系

    项目目录概况 Demo01项目 Test01.java package com.sam.demo01; public class Test01 { public void ShowTest01() { ...

  7. javascript常用数据验证函数

    正则表达式日期验证函数 function CheckDate(str){       //在JavaScript中,正则表达式只能使用"/"开头和结束,不能使用双引号       ...

  8. fatal error: iconv.h: No such file or directory

    CodeLite CodeLite编译(使用Cygwin Toolchain)出现如下错误: fatal error: iconv.h: No such file or directory 解决办法 ...

  9. ucoreOS_lab 1~8 实验报告导航

    所有的实验已经全部完成,实验的源代码及报告都在 Github 上,欢迎大家批评指正,如果觉得对你有帮助的话,欢迎为此项目 star & watch & fork 三连,让更多的朋友们看 ...

  10. Xamarin.Forms 移动开发

    Xamarin 提供两种原生app开发技术:1. Xamarin Native, 包括 Xamarin.Android, Xamarin.iOS, Xamarin.Mac 2. Xamarin 跨平台 ...