当前位置: 首页 > news >正文

fileinputstream读取文件_20M 文件用 Java 压缩从30秒到1秒的优化过程

有一个需求需要将前端传过来的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:20Mconsum 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 -1 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();}}
输出
------BufferfileSize:20Mconsum 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阶段就是从内核空间转到用户空间的一个过程
可以看到速度相比较使用缓冲区已经有了一些的提高。
------ChannelfileSize:20Mconsum time:1416

内核空间和用户空间

那么为什么从内核空间转向用户空间这段过程会慢呢?首先我们需了解的是什么是内核空间和用户空间。在常用的操作系统中为了保护系统中的核心资源,于是将系统设计为四个区域,越往里权限越大,所以Ring0被称之为内核空间,用来访问一些关键性的资源。Ring3被称之为用户空间。 b602aca32773c10ab7cd1f5fbaf6fa4d.png image
用户态、内核态:线程处于内核空间称之为内核态,线程处于用户空间属于用户态
那么我们如果此时应用程序(应用程序是都属于用户态的)需要访问核心资源怎么办呢?那就需要调用内核中所暴露出的接口用以调用,称之为系统调用。例如此时我们应用程序需要访问磁盘上的文件。此时应用程序就会调用系统调用的接口 open方法,然后内核去访问磁盘中的文件,将文件内容返回给应用程序。大致的流程如下 b9af37af9e96fc20d2c5992e3d6ffacd.png image

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

既然我们要读取一个磁盘的文件,要废这么大的周折。有没有什么简单的方法能够使我们的应用直接操作磁盘文件,不需要内核进行中转呢?有,那就是建立直接缓冲区了。 非直接缓冲区: 非直接缓冲区就是我们上面所讲内核态作为中间人,每次都需要内核在中间作为中转。
![](https://upload-images.jianshu.io/upload_images/13146186-6bbfb5446221cb2d.jpg?imageMogr2/auto-orient/strip|imageView2/2/w/812/format/webp)image
直接缓冲区: 直接缓冲区不需要内核空间作为中转copy数据,而是直接在物理内存申请一块空间,这块空间映射到内核地址空间和用户地址空间,应用程序与磁盘之间数据的存取通过这块直接申请的物理内存进行交互。 3a8b3fa305874c19ab9868171a8ad987.png 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();}}
打印如下
---------MapfileSize:20Mconsum 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
71c121167e1a39e34c657c03f5fad34c.png image 我想要的效果是这样的。源码如下
//Version 5 使用Pippublic 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();}}

总结

生活处处都需要学习,有时候只是一个简单的优化,可以让你深入学习到各种不同的知识。所以在学习中要不求甚解,不仅要知道这个知识也要了解为什么要这么做。 知行合一:学习完一个知识要尽量应用一遍。这样才能记得牢靠

参考文章

https://www.jianshu.com/p/f90866dcbffc https://juejin.im/post/5af942c6f265da0b7026050c
来源:https://www.jianshu.com/p/25b328753017

-END-

41437bf872283946d68db702c416a112.png

相关文章:

  • python打印皮卡丘_用python打印你的宠物小精灵吧
  • 平方根python_python的平方根
  • python怎么测试c代码_可以使用基于Python的单元测试框架和跑步者来测试C代码
  • nginx 跨域访问配置_nginx配置用户访问认证
  • python爬虫xpath教程_Spider-Python爬虫之XPath 教程
  • python常见报错类型_python打印错误类型
  • 华为官方解锁工具_开启“应用锁”和“健康使用手机密码”的华为,哪个功能对于用户隐私更安全?...
  • vant coupon 时间戳如何计算_flink入门(八)中的时间戳如何使用?Watermark使用及原理...
  • python按esc结束循环_當按ESC鍵時,立即讓python退出程序
  • python 元组和列表区别_Python高级数据类型之列表、元组
  • 微软商店账户登录一直转圈_Win 10再曝致命BUG,微软:暂不清楚问题根源
  • vlookup两个条件匹配_自从学会了INDEX+MATCH,我就再也不用VLOOKUP了
  • 32位svn 64位操作系统_如何选择安装32位系统还是64位系统
  • python生产和消费模型_Python线程+队列实现生产消费模型,Pythonthreadingqueue
  • python中变量_Python中的变量与常量
  • 【EOS】Cleos基础
  • android高仿小视频、应用锁、3种存储库、QQ小红点动画、仿支付宝图表等源码...
  • CSS3 聊天气泡框以及 inherit、currentColor 关键字
  • ES10 特性的完整指南
  • Java方法详解
  • React-Native - 收藏集 - 掘金
  • spring boot下thymeleaf全局静态变量配置
  • storm drpc实例
  • V4L2视频输入框架概述
  • 对话:中国为什么有前途/ 写给中国的经济学
  • 分布式任务队列Celery
  • 高度不固定时垂直居中
  • 给初学者:JavaScript 中数组操作注意点
  • 如何优雅的使用vue+Dcloud(Hbuild)开发混合app
  • 微信开放平台全网发布【失败】的几点排查方法
  • 小程序 setData 学问多
  • 一文看透浏览器架构
  • LevelDB 入门 —— 全面了解 LevelDB 的功能特性
  • 第二十章:异步和文件I/O.(二十三)
  • 机器人开始自主学习,是人类福祉,还是定时炸弹? ...
  • 专访Pony.ai 楼天城:自动驾驶已经走过了“从0到1”,“规模”是行业的分水岭| 自动驾驶这十年 ...
  • ​LeetCode解法汇总1276. 不浪费原料的汉堡制作方案
  • ​批处理文件中的errorlevel用法
  • #define MODIFY_REG(REG, CLEARMASK, SETMASK)
  • #NOIP 2014# day.2 T2 寻找道路
  • #我与Java虚拟机的故事#连载09:面试大厂逃不过的JVM
  • #在线报价接单​再坚持一下 明天是真的周六.出现货 实单来谈
  • $.ajax()参数及用法
  • $redis-setphp_redis Set命令,php操作Redis Set函数介绍
  • (javascript)再说document.body.scrollTop的使用问题
  • (附源码)ssm基于jsp高校选课系统 毕业设计 291627
  • (含react-draggable库以及相关BUG如何解决)固定在左上方某盒子内(如按钮)添加可拖动功能,使用react hook语法实现
  • (七)MySQL是如何将LRU链表的使用性能优化到极致的?
  • (十)T检验-第一部分
  • (一)ClickHouse 中的 `MaterializedMySQL` 数据库引擎的使用方法、设置、特性和限制。
  • (原創) 如何將struct塞進vector? (C/C++) (STL)
  • (转)Google的Objective-C编码规范
  • (转)IOS中获取各种文件的目录路径的方法
  • (转)甲方乙方——赵民谈找工作
  • ... 是什么 ?... 有什么用处?