自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

Java壓縮20M文件從30秒到1秒的優(yōu)化過(guò)程

開發(fā) 后端
有一個(gè)需求需要將前端傳過(guò)來(lái)的10張照片,然后后端進(jìn)行處理以后壓縮成一個(gè)壓縮包通過(guò)網(wǎng)絡(luò)流傳輸出去。之前沒有接觸過(guò)用Java壓縮文件的,所以就直接上網(wǎng)找了一個(gè)例子改了一下用了,改完以后也能使用,但是隨著前端所傳圖片的大小越來(lái)越大的時(shí)候,耗費(fèi)的時(shí)間也在急劇增加,最后測(cè)了一下壓縮20M...

有一個(gè)需求需要將前端傳過(guò)來(lái)的10張照片,然后后端進(jìn)行處理以后壓縮成一個(gè)壓縮包通過(guò)網(wǎng)絡(luò)流傳輸出去。之前沒有接觸過(guò)用Java壓縮文件的,所以就直接上網(wǎng)找了一個(gè)例子改了一下用了,改完以后也能使用,但是隨著前端所傳圖片的大小越來(lái)越大的時(shí)候,耗費(fèi)的時(shí)間也在急劇增加,最后測(cè)了一下壓縮20M的文件竟然需要30秒的時(shí)間。壓縮文件的代碼如下。 

  1. public static void zipFileNoBuffer() { 
  2.     File zipFile = new File(ZIP_FILE); 
  3.     try (ZipOutputStream zipOut = new ZipOutputStream(new FileOutputStream(zipFile))) { 
  4.         //開始時(shí)間 
  5.         long beginTime = System.currentTimeMillis(); 
  6.  
  7.         for (int i = 0; i < 10; i++) { 
  8.             try (InputStream input = new FileInputStream(JPG_FILE)) { 
  9.                 zipOut.putNextEntry(new ZipEntry(FILE_NAME + i)); 
  10.                 int temp = 0; 
  11.                 while ((temp = input.read()) != -1) { 
  12.                     zipOut.write(temp); 
  13.                 } 
  14.             } 
  15.         } 
  16.         printInfo(beginTime); 
  17.     } catch (Exception e) { 
  18.         e.printStackTrace(); 
  19.     } 

這里找了一張2M大小的圖片,并且循環(huán)十次進(jìn)行測(cè)試。打印的結(jié)果如下,時(shí)間大概是30秒。 

  1. fileSize:20M 
  2. consum time:29599 

第一次優(yōu)化過(guò)程-從30秒到2秒

進(jìn)行優(yōu)化首先想到的是利用緩沖區(qū) BufferInputStream。在 FileInputStream中 read()方法每次只讀取一個(gè)字節(jié)。源碼中也有說(shuō)明。 

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

這是一個(gè)調(diào)用本地方法與原生操作系統(tǒng)進(jìn)行交互,從磁盤中讀取數(shù)據(jù)。每讀取一個(gè)字節(jié)的數(shù)據(jù)就調(diào)用一次本地方法與操作系統(tǒng)交互,是非常耗時(shí)的。例如我們現(xiàn)在有30000個(gè)字節(jié)的數(shù)據(jù),如果使用 FileInputStream那么就需要調(diào)用30000次的本地方法來(lái)獲取這些數(shù)據(jù),而如果使用緩沖區(qū)的話(這里假設(shè)初始的緩沖區(qū)大小足夠放下30000字節(jié)的數(shù)據(jù))那么只需要調(diào)用一次就行。因?yàn)榫彌_區(qū)在第一次調(diào)用 read()方法的時(shí)候會(huì)直接從磁盤中將數(shù)據(jù)直接讀取到內(nèi)存中。隨后再一個(gè)字節(jié)一個(gè)字節(jié)的慢慢返回。

BufferedInputStream內(nèi)部封裝了一個(gè)byte數(shù)組用于存放數(shù)據(jù),默認(rèn)大小是8192

優(yōu)化過(guò)后的代碼如下 

  1. public static void zipFileBuffer() { 
  2.     File zipFile = new File(ZIP_FILE); 
  3.     try (ZipOutputStream zipOut = new ZipOutputStream(new FileOutputStream(zipFile)); 
  4.             BufferedOutputStream bufferedOutputStream = new BufferedOutputStream(zipOut)) { 
  5.         //開始時(shí)間 
  6.         long beginTime = System.currentTimeMillis(); 
  7.         for (int i = 0; i < 10; i++) { 
  8.             try (BufferedInputStream bufferedInputStream = new BufferedInputStream(new FileInputStream(JPG_FILE))) { 
  9.                 zipOut.putNextEntry(new ZipEntry(FILE_NAME + i)); 
  10.                 int temp = 0; 
  11.                 while ((temp = bufferedInputStream.read()) != -1) { 
  12.                     bufferedOutputStream.write(temp); 
  13.                 } 
  14.             } 
  15.         } 
  16.         printInfo(beginTime); 
  17.     } catch (Exception e) { 
  18.         e.printStackTrace(); 
  19.     } 

輸出 

  1. ------Buffer 
  2. fileSize:20M 
  3. consum time:1808 

可以看到相比較于第一次使用 FileInputStream效率已經(jīng)提升了許多了

第二次優(yōu)化過(guò)程-從2秒到1秒

使用緩沖區(qū) buffer的話已經(jīng)是滿足了我的需求了,但是秉著學(xué)以致用的想法,就想著用NIO中知識(shí)進(jìn)行優(yōu)化一下。

使用Channel

為什么要用 Channel呢?因?yàn)樵贜IO中新出了 Channel和 ByteBuffer。正是因?yàn)樗鼈兊慕Y(jié)構(gòu)更加符合操作系統(tǒng)執(zhí)行I/O的方式,所以其速度相比較于傳統(tǒng)IO而言速度有了顯著的提高。Channel就像一個(gè)包含著煤礦的礦藏,而 ByteBuffer則是派送到礦藏的卡車。也就是說(shuō)我們與數(shù)據(jù)的交互都是與 ByteBuffer的交互。

在NIO中能夠產(chǎn)生 FileChannel的有三個(gè)類。分別是 FileInputStream、 FileOutputStream、以及既能讀又能寫的 RandomAccessFile。

源碼如下 

  1. public static void zipFileChannel() { 
  2.     //開始時(shí)間 
  3.     long beginTime = System.currentTimeMillis(); 
  4.     File zipFile = new File(ZIP_FILE); 
  5.     try (ZipOutputStream zipOut = new ZipOutputStream(new FileOutputStream(zipFile)); 
  6.             WritableByteChannel writableByteChannel = Channels.newChannel(zipOut)) { 
  7.         for (int i = 0; i < 10; i++) { 
  8.             try (FileChannel fileChannel = new FileInputStream(JPG_FILE).getChannel()) { 
  9.                 zipOut.putNextEntry(new ZipEntry(i + SUFFIX_FILE)); 
  10.                 fileChannel.transferTo(0, FILE_SIZE, writableByteChannel); 
  11.             } 
  12.         } 
  13.         printInfo(beginTime); 
  14.     } catch (Exception e) { 
  15.         e.printStackTrace(); 
  16.     } 

我們可以看到這里并沒有使用 ByteBuffer進(jìn)行數(shù)據(jù)傳輸,而是使用了 transferTo的方法。這個(gè)方法是將兩個(gè)通道進(jìn)行直連。 

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

這是源碼上的描述文字,大概意思就是使用 transferTo的效率比循環(huán)一個(gè) Channel讀取出來(lái)然后再循環(huán)寫入另一個(gè) Channel好。操作系統(tǒng)能夠直接傳輸字節(jié)從文件系統(tǒng)緩存到目標(biāo)的 Channel中,而不需要實(shí)際的 copy階段。

copy階段就是從內(nèi)核空間轉(zhuǎn)到用戶空間的一個(gè)過(guò)程

可以看到速度相比較使用緩沖區(qū)已經(jīng)有了一些的提高。 

  1. ------Channel 
  2. fileSize:20M 
  3. consum time:1416 

內(nèi)核空間和用戶空間

那么為什么從內(nèi)核空間轉(zhuǎn)向用戶空間這段過(guò)程會(huì)慢呢?首先我們需了解的是什么是內(nèi)核空間和用戶空間。在常用的操作系統(tǒng)中為了保護(hù)系統(tǒng)中的核心資源,于是將系統(tǒng)設(shè)計(jì)為四個(gè)區(qū)域,越往里權(quán)限越大,所以Ring0被稱之為內(nèi)核空間,用來(lái)訪問(wèn)一些關(guān)鍵性的資源。Ring3被稱之為用戶空間。 

 

 

 

用戶態(tài)、內(nèi)核態(tài):線程處于內(nèi)核空間稱之為內(nèi)核態(tài),線程處于用戶空間屬于用戶態(tài)

那么我們?nèi)绻藭r(shí)應(yīng)用程序(應(yīng)用程序是都屬于用戶態(tài)的)需要訪問(wèn)核心資源怎么辦呢?那就需要調(diào)用內(nèi)核中所暴露出的接口用以調(diào)用,稱之為系統(tǒng)調(diào)用。例如此時(shí)我們應(yīng)用程序需要訪問(wèn)磁盤上的文件。此時(shí)應(yīng)用程序就會(huì)調(diào)用系統(tǒng)調(diào)用的接口 open方法,然后內(nèi)核去訪問(wèn)磁盤中的文件,將文件內(nèi)容返回給應(yīng)用程序。大致的流程如下 

 

 

 

直接緩沖區(qū)和非直接緩沖區(qū)

既然我們要讀取一個(gè)磁盤的文件,要廢這么大的周折。有沒有什么簡(jiǎn)單的方法能夠使我們的應(yīng)用直接操作磁盤文件,不需要內(nèi)核進(jìn)行中轉(zhuǎn)呢?有,那就是建立直接緩沖區(qū)了。

非直接緩沖區(qū):非直接緩沖區(qū)就是我們上面所講內(nèi)核態(tài)作為中間人,每次都需要內(nèi)核在中間作為中轉(zhuǎn)。 

 

 

 

直接緩沖區(qū):直接緩沖區(qū)不需要內(nèi)核空間作為中轉(zhuǎn)copy數(shù)據(jù),而是直接在物理內(nèi)存申請(qǐng)一塊空間,這塊空間映射到內(nèi)核地址空間和用戶地址空間,應(yīng)用程序與磁盤之間數(shù)據(jù)的存取通過(guò)這塊直接申請(qǐng)的物理內(nèi)存進(jìn)行交互。 

 

 

 

既然直接緩沖區(qū)那么快,我們?yōu)槭裁床欢加弥苯泳彌_區(qū)呢?其實(shí)直接緩沖區(qū)有以下的缺點(diǎn)。直接緩沖區(qū)的缺點(diǎn):

1、不安全

2、消耗更多,因?yàn)樗皇窃贘VM中直接開辟空間。這部分內(nèi)存的回收只能依賴于垃圾回收機(jī)制,垃圾什么時(shí)候回收不受我們控制。

3、數(shù)據(jù)寫入物理內(nèi)存緩沖區(qū)中,程序就喪失了對(duì)這些數(shù)據(jù)的管理,即什么時(shí)候這些數(shù)據(jù)被最終寫入從磁盤只能由操作系統(tǒng)來(lái)決定,應(yīng)用程序無(wú)法再干涉。

綜上所述,所以我們使用 transferTo方法就是直接開辟了一段直接緩沖區(qū)。所以性能相比而言提高了許多

使用內(nèi)存映射文件

NIO中新出的另一個(gè)特性就是內(nèi)存映射文件,內(nèi)存映射文件為什么速度快呢?其實(shí)原因和上面所講的一樣,也是在內(nèi)存中開辟了一段直接緩沖區(qū)。與數(shù)據(jù)直接作交互。源碼如下 

  1. //Version 4 使用Map映射文件 
  2. public static void zipFileMap() { 
  3.     //開始時(shí)間 
  4.     long beginTime = System.currentTimeMillis(); 
  5.     File zipFile = new File(ZIP_FILE); 
  6.     try (ZipOutputStream zipOut = new ZipOutputStream(new FileOutputStream(zipFile)); 
  7.             WritableByteChannel writableByteChannel = Channels.newChannel(zipOut)) { 
  8.         for (int i = 0; i < 10; i++) { 
  9.  
  10.             zipOut.putNextEntry(new ZipEntry(i + SUFFIX_FILE)); 
  11.  
  12.             //內(nèi)存中的映射文件 
  13.             MappedByteBuffer mappedByteBuffer = new RandomAccessFile(JPG_FILE_PATH, "r").getChannel() 
  14.                     .map(FileChannel.MapMode.READ_ONLY, 0, FILE_SIZE); 
  15.  
  16.             writableByteChannel.write(mappedByteBuffer); 
  17.         } 
  18.         printInfo(beginTime); 
  19.     } catch (Exception e) { 
  20.         e.printStackTrace(); 
  21.     } 

打印如下 

  1. ---------Map 
  2. fileSize:20M 
  3. consum time:1305 

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

使用Pipe

Java NIO 管道是2個(gè)線程之間的單向數(shù)據(jù)連接。Pipe有一個(gè)source通道和一個(gè)sink通道。其中source通道用于讀取數(shù)據(jù),sink通道用于寫入數(shù)據(jù)??梢钥吹皆创a中的介紹,大概意思就是寫入線程會(huì)阻塞至有讀線程從通道中讀取數(shù)據(jù)。如果沒有數(shù)據(jù)可讀,讀線程也會(huì)阻塞至寫線程寫入數(shù)據(jù)。直至通道關(guān)閉。 

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

 

 

 

我想要的效果是這樣的。源碼如下 

  1. //Version 5 使用Pip 
  2. public static void zipFilePip() { 
  3.  
  4.     long beginTime = System.currentTimeMillis(); 
  5.     try(WritableByteChannel out = Channels.newChannel(new FileOutputStream(ZIP_FILE))) { 
  6.         Pipe pipe = Pipe.open(); 
  7.         //異步任務(wù) 
  8.         CompletableFuture.runAsync(()->runTask(pipe)); 
  9.  
  10.         //獲取讀通道 
  11.         ReadableByteChannel readableByteChannel = pipe.source(); 
  12.         ByteBuffer buffer = ByteBuffer.allocate(((int) FILE_SIZE)*10); 
  13.         while (readableByteChannel.read(buffer)>= 0) { 
  14.             buffer.flip(); 
  15.             out.write(buffer); 
  16.             buffer.clear(); 
  17.         } 
  18.     }catch (Exception e){ 
  19.         e.printStackTrace(); 
  20.     } 
  21.     printInfo(beginTime); 
  22.  
  23.  
  24. //異步任務(wù) 
  25. public static void runTask(Pipe pipe) { 
  26.  
  27.     try(ZipOutputStream zos = new ZipOutputStream(Channels.newOutputStream(pipe.sink())); 
  28.             WritableByteChannel out = Channels.newChannel(zos)) { 
  29.         System.out.println("Begin"); 
  30.         for (int i = 0; i < 10; i++) { 
  31.             zos.putNextEntry(new ZipEntry(i+SUFFIX_FILE)); 
  32.  
  33.             FileChannel jpgChannel = new FileInputStream(new File(JPG_FILE_PATH)).getChannel(); 
  34.  
  35.             jpgChannel.transferTo(0, FILE_SIZE, out); 
  36.  
  37.             jpgChannel.close(); 
  38.         } 
  39.     }catch (Exception e){ 
  40.         e.printStackTrace(); 
  41.     } 

總結(jié)

生活處處都需要學(xué)習(xí),有時(shí)候只是一個(gè)簡(jiǎn)單的優(yōu)化,可以讓你深入學(xué)習(xí)到各種不同的知識(shí)。所以在學(xué)習(xí)中要不求甚解,不僅要知道這個(gè)知識(shí)也要了解為什么要這么做。

知行合一:學(xué)習(xí)完一個(gè)知識(shí)要盡量應(yīng)用一遍。這樣才能記得牢靠。

源碼地址

https://github.com/modouxiansheng/Doraemon 

 

責(zé)任編輯:龐桂玉 來(lái)源: 不學(xué)無(wú)數(shù)的程序員
相關(guān)推薦

2019-08-21 14:35:18

壓縮文件優(yōu)化過(guò)程Java

2017-12-25 11:15:06

JavaArray數(shù)組

2022-09-26 09:41:25

MySQL數(shù)據(jù)庫(kù)

2021-08-26 22:26:55

性能優(yōu)化技術(shù)

2025-03-27 00:14:10

2019-01-30 09:34:56

ElasticSearLogstashKibana

2013-11-11 11:17:45

AngularJS性能優(yōu)化

2011-09-29 09:57:27

Ubuntu 11.1

2012-07-24 09:37:19

寬帶光纖

2024-11-08 09:34:54

2010-08-19 09:36:28

飛輪儲(chǔ)能UPS飛輪UPS

2024-05-06 10:11:51

2015-11-23 17:34:33

秒借

2024-11-01 12:10:57

2019-09-27 17:24:26

數(shù)據(jù)庫(kù)優(yōu)化sql

2024-11-08 15:08:17

2016-11-21 08:26:36

2021-03-19 09:53:28

Python 開發(fā)編程語(yǔ)言

2015-04-20 11:22:04

SQL慢查詢優(yōu)化

2018-12-21 10:39:31

華為
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號(hào)