有一个要求,前端传来的10张照片需要经过后端处理,压缩成压缩包,通过网络传输。之前没用过Java压缩文件,直接在网上找了个例子改了下。修改后可以使用,但是随着前端传过来的图片尺寸越来越大,耗时也急剧增加。最后测了下,压缩一个20M的文件需要30秒。压缩文件的代码如下。publicstaticvoidzipFileNoBuffer(){FilezipFile=newFile(ZIP_FILE);try(ZipOutputStreamzipOut=newZipOutputStream(newFileOutputStream(zipFile))){//开始时间longbeginTime=System.currentTimeMillis();for(inti=0;i<10;i++){尝试(InputStreaminput=newFileInputStream(JPG_FILE)){zipOut.putNextEntry(newZipEntry(FILE_NAME+i));inttemp=0;while((temp=input.read())!=-1){zipOut.write(temp);}}}printInfo(beginTime);}catch(Exceptione){e.printStackTrace();}这里找了一张2M的图片,循环十次进行测试。打印结果如下,时间约30秒。fileSize:20Mconsumtime:29599第一次优化过程——从30秒优化到2秒首先想到的是使用缓冲区BufferInputStream。FileInputStream中的read()方法一次只读取一个字节。源码中也有说明。/***从这个输入流中读取一个字节的数据。如果没有可用的输入,这个方法会阻止。**@returnthenextbyteofdata,或者-1
如果到达*文件的结尾。*@exceptionIOExceptionifanI/Oerroroccurs.*/publicnativeintread()throwsIOException;操作系统交互以从磁盘读取数据。每次读取一个字节的数据都要调用native方法与操作系统交互,非常耗时。例如,我们现在有30,000字节的数据。如果使用FileInputStream,需要调用本地方法30000次才能拿到数据。如果使用缓冲区(这里假设初始缓冲区大小足以容纳30,000字节的数据)那么只需要调用一次即可。因为第一次调用read()方法时buffer会直接从磁盘中读取数据直接读到内存中。然后一个字节一个字节的慢慢返回。BufferedInputStream内部封装了一个字节数组用于存储数据。默认大小为8192,优化代码如下){//开始时间longbeginTime=System.currentTimeMillis();for(inti=0;i<10;i++){try(BufferedInputStreambufferedInputStream=newBufferedInputStream(newFileInputStream(JPG_FILE))){zipOut.putNextEntry(newZipEntry(FILE_NAME+i));inttemp=0;while((temp=bufferedInputStream.read())!=-1){bufferedOutputStream.write(temp);}}}printInfo(beginTime);}catch(Exceptione){e.printStackTrace();}}Output------BufferfileSize:20Mconsumtime:1808可以看出,与第一次使用FileInputStream相比,效率提升了很多。第二个优化过程——从2秒到1秒使用缓冲buffer已经可以满足我的需求了,但是本着学以致用的想法,想利用NIO中的知识来优化一下。为什么要使用频道?因为Channel和ByteBuffer是NIO中新增的。正是因为它们的结构更符合操作系统执行I/O的方式,所以其速度相对于传统IO有了明显的提升。Channel就像一个包含煤矿的矿井,而ByteBuffer是一辆被派往矿井的卡车。也就是说,我们与数据的交互,都是与ByteBuffer的交互。NIO中可以生成FileChannel的类有3个。分别是FileInputStream、FileOutputStream、RandomAccessFile,既可以读也可以写。源码如下publicstaticvoidzipFileChannel(){//开始时间{Out){=0;i<10;i++){try(FileChannelfileChannel=newFileInputStream(JPG_FILE).getChannel()){zipOut.putNextEntry(newZipEntry(i+SUFFIX_FILE));fileChannel.transferTo(0,FILE_SIZE,writableByteChannel);}}printInfo(beginTime);}catch(Exceptione){e.printStackTrace();}}我们可以看到数据传输时并没有使用ByteBuffer,而是使用了transferTo方法。这种方法是直接连接两个通道。这种方法可能比从该通道读取并写入目标通道的简单循环更有效。许多*操作系统可以将字节直接从文件系统缓存*传输到目标通道,而无需实际复制它们。这是源码上的描述文字,大概意思是使用transferTo的效率比循环一个Channel读取然后循环写入另一个Channel更好操作系统可以直接从文件系统缓存中传输字节到目标没有实际复制阶段的通道。复制阶段是从内核空间到用户空间的一个过程。可以看出,相比使用缓冲区,速度得到了提升。------ChannelfileSize:20Mconsumtime:1416内核空间和用户空间那么为什么从内核空间到用户空间的过程很慢呢?首先,我们需要了解什么是内核空间和用户空间。在常用的操作系统中为了保护系统的核心资源,系统被设计成四个区域,越往里越权限递增,所以Ring0被称为内核空间,用来访问一些关键资源.Ring3称为用户空间。用户态和内核态:内核空间的线程称为内核态,用户空间的线程属于用户态,那么此时应用程序(所有应用程序都属于用户态)需要访问核心资源怎么办?那么就需要调用暴露在内核中的接口来调用,这就是系统调用。例如,此时我们的应用程序需要访问磁盘上的文件。这时,应用程序会调用系统调用的接口open方法,然后内核会访问磁盘中的文件,并将文件内容返回给应用程序。大体流程如下:直接缓冲和非直接缓冲既然我们要读取一个磁盘文件,就得浪费这么大的挫折。有没有什么简单的方法可以让我们的应用程序直接操作磁盘文件而不需要内核中转呢?对,就是建立一个直接缓冲区。Non-directbuffer:非直接buffer就是我们上面提到的内核态作为中间人,内核每次都需要在中间作为中转。直接缓冲区:直接缓冲区不需要内核空间作为传输拷贝数据,而是直接在物理内存中申请一块空间,映射到内核地址空间和用户地址空间,以及应用程序之间的数据访问和磁盘通过这种直接申请的物理内存进行交互。既然directbuffer这么快,为什么我们不都用directbuffer呢?事实上,直接缓冲区有以下缺点。directbuffer的缺点:1.不安全2.比较消耗,因为不直接在JVM开辟空间。这部分内存的回收只能依靠垃圾回收机制,而垃圾什么时候回收是我们无法控制的。3.当数据写入物理内存缓冲区后,程序就失去了对这些数据的管理,即这些数据最终何时写入从盘只能由操作系统来决定,应用程序无法再干扰。综上所述,所以我们使用transferTo方法直接开辟了一个directbuffer。因此,与使用内存映射文件相比,性能提升了很多。NIO的另一个新特性是内存映射文件。为什么内存映射文件很快?其实道理和上面说的一样,也是在内存中开辟了一段。直接缓冲。直接与数据交互。源码如下//Version4使用Map映射文件publicstaticvoidzipFileMap(){//开始时间.Channels.Channels(newzipOut)){for(inti=0;i<10;i++){zipOut.putNextEntry(newZipEntry(i+SUFFIX_FILE));//MappedByteBuffermappedByteBuffer=newRandomAccessFile(JPG_FILE_PATH,"r").getChannel().map(FileChannel.MapMode.READ_ONLY,0,FILE_SIZE);writableByteChannel.write(mappedByteBuffer);}printInfo(beginTime);}catch(Exceptione){e.printStackTrace();}}打印如下---------MapfileSize:20Mconsumtime:1305可以看到速度和使用Channel差不多。使用PipeJavaNIO管道是2个线程之间的单向数据连接。管道有一个源通道和一个汇通道。sourcechannel用于读取数据,sinkchannel用于写入数据。可以看源码中的介绍,大概意思就是写线程会一直阻塞,直到读线程从通道中读取数据。如果没有数据可读,reader线程也会阻塞,直到writer线程写入数据。直到通道关闭。线程将字节写入apipe是否会阻塞,直到另一个线程读取这些字节我想要的效果是这样的。源码如下//Version5使用PippublicstaticvoidzipFilePip(){longbeginTime=System.currentTimeMillis();try(WritableByteChannelout=Channels.newChannel(newFileOutputStream(ZIP_FILE))){Pipepipe=Pipe.open();//异常步骤CompletableFuture.runAsync(()->runTask(pipe));//获取读取通道ReadableByteChannelreadableByteChannel=pipe.source();ByteBufferbuffer=ByteBuffer.allocate(((int)FILE_SIZE)*10);while(readableByteChannel.read(buffer)>=0){buffer.flip();out.write(buffer);buffer.clear();}}catch(Exceptione){e.printStackTrace();}printInfo(beginTime);}//异步任务publicstaticvoidrunTask(Pipepipe){try(ZipOutputStreamzos=newZipOutputStream(Channels.newOutputStream(pipe.sink()));WritableByteChannelout=Channels.newChannel(zos)){System.out.println("开始");for(inti=0;i<10;i++){zos.putNextEntry(newZipEntry(i+SUFFIX_FILE));FileChanneljpgChannel=newFileInputStream(newFile(JPG_FILE_PATH)).getChannel();jpgChannel.transferTo(0,FILE_SIZE,out);jpgChannel.close();}}catch(异常){e.printStackTrace();}}总结生活中处处需要学习,有时候只是简单的优化,让你深入学习各种知识。知行合一:学完一门知识,再尝试应用。这样你就可以牢牢记住它。源码地址https://github.com/modouxiansheng/Doraemon
