壓縮 20M 文件從 30 秒到 1 秒的優(yōu)化過程
點(diǎn)擊上方藍(lán)色“程序猿DD”,選擇“設(shè)為星標(biāo)”
回復(fù)“資源”獲取獨(dú)家整理的學(xué)習(xí)資料!

有一個(gè)需求需要將前端傳過來的10張照片,然后進(jìn)行進(jìn)行處理以后壓縮成一個(gè)壓縮包通過網(wǎng)絡(luò)流傳輸出去。之前沒有接觸過用Java壓縮文件的,所以就直接上網(wǎng)找了一個(gè)例子改了一下用了,改完以后也能使用,但是連續(xù)前端所傳圖片的大小越來越大的時(shí)候,耗費(fèi)的時(shí)間同時(shí)急劇增加,最后測(cè)了一下壓縮20M的文件竟然需要30秒的時(shí)間。壓縮文件的代碼如下。
public?static?void?zipFileNoBuffer()?{
????File?zipFile?=?new?File(ZIP_FILE);
????try?(ZipOutputStream?zipOut?=?new?ZipOutputStream(new?FileOutputStream(zipFile)))?{
????????//開始時(shí)間
????????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大小的圖片,并且循環(huán)十次進(jìn)行測(cè)試。打印的結(jié)果如下,時(shí)間大概是30秒。
fileSize:20M
consum?time:29599
第一次優(yōu)化過程-從30秒到2秒
優(yōu)化進(jìn)行想到首先的的英文利用緩沖區(qū)BufferInputStream。在FileInputStream中read()方法每次只讀取一個(gè)字節(jié)。源碼中也有說明。
/**
?*?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;
這是一個(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次的本地方法來獲取這些數(shù)據(jù),而如果使用標(biāo)題的話(這里假設(shè)初始的尺寸大小足夠放下30000字節(jié)的數(shù)據(jù))那么只需要調(diào)用一次就行。原因在于在第一次調(diào)用read()方法的時(shí)候會(huì)直接從磁盤中將數(shù)據(jù)直接讀取到內(nèi)存中。隨后再一個(gè)字節(jié)一個(gè)字節(jié)的慢慢返回。
BufferedInputStream內(nèi)部封裝了一個(gè)字節(jié)的用于存放數(shù)據(jù),最小大小是8192
優(yōu)化過后的代碼如下
public?static?void?zipFileBuffer()?{
????File?zipFile?=?new?File(ZIP_FILE);
????try?(ZipOutputStream?zipOut?=?new?ZipOutputStream(new?FileOutputStream(zipFile));
????????????BufferedOutputStream?bufferedOutputStream?=?new?BufferedOutputStream(zipOut))?{
????????//開始時(shí)間
????????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效率已經(jīng)提升了很多了
第二次優(yōu)化過程-從2秒到1秒
使用buffer正規(guī)的話已經(jīng)是滿足了我的需求了,但是秉著學(xué)以致用的想法,就想著用NIO中知識(shí)進(jìn)行優(yōu)化一下。
使用頻道
要用為什么Channel呢?因?yàn)樵贜IO新中出了Channel狀語從句:ByteBuffer。正是因?yàn)樗鼈兊慕Y(jié)構(gòu)更加符合操作系統(tǒng)執(zhí)行I / O的方式,所以其速度相比較于傳統(tǒng)IO而言速度有了顯著的提高。Channel就像一個(gè)包含著煤礦的礦藏,而ByteBuffer則是派遣煤礦藏的卡車。如此我們與數(shù)據(jù)的相互作用都是與ByteBuffer的相互作用。
在NIO能夠中產(chǎn)生FileChannel的有三個(gè)類,分別是FileInputStream, ,FileOutputStream以及既能讀又能寫的RandomAccessFile。
源碼如下
public?static?void?zipFileChannel()?{
????//開始時(shí)間
????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進(jìn)行數(shù)據(jù)傳輸,甚至使用了transferTo的方法。這個(gè)方法是將兩個(gè)通道進(jìn)行直連。
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.
這是二進(jìn)制上的描述文字,大概意思就是使用transferTo的效率比循環(huán)一個(gè)Channel讀取出來然后重啟寫入另一個(gè)Channel好。操作系統(tǒng)能夠直接傳輸字節(jié)從文件系統(tǒng)緩存到目標(biāo)的Channel中,而不需要實(shí)際的copy階段。
復(fù)制階段就是從內(nèi)核空間轉(zhuǎn)到用戶空間的一個(gè)過程
可以看到速度相比較使用范圍已經(jīng)有了一些的提高。
------Channel
fileSize:20M
consum?time:1416
內(nèi)核空間和用戶空間
那么為什么從內(nèi)核空間轉(zhuǎn)向用戶空間階段過程會(huì)慢呢??首先我們需要了解的是什么是內(nèi)核空間和用戶空間。在常用的操作系統(tǒng)中為了保護(hù)系統(tǒng)中的核心資源,于是將系統(tǒng)設(shè)計(jì)為四個(gè)區(qū)域,越往里權(quán)限限制,所以Ring0被稱為內(nèi)核空間,用來訪問一些關(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)的)需要訪問核心資源怎么辦呢?那就需要調(diào)用內(nèi)核中所暴露的接口調(diào)用,稱為系統(tǒng)調(diào)用。例如此時(shí)我們應(yīng)用程序需要訪問磁盤上的文件。此時(shí)應(yīng)用程序就會(huì)調(diào)用系統(tǒng)調(diào)用的接口open方法,然后內(nèi)核去訪問磁盤中的文件,將文件內(nèi)容返回給應(yīng)用程序。

直接罪行和非直接罪行
既然我們要讀取一個(gè)磁盤的文件,要廢這么大的周折。有沒有什么簡單的方法能夠使我們的應(yīng)用直接操作磁盤文件,不需要內(nèi)核進(jìn)行中轉(zhuǎn)呢?有,那就是建立直接歸檔了。
非直接競爭:非直接歧視就是我們上面所講的內(nèi)核態(tài)作為中間人,每次都需要內(nèi)核在中間作為中轉(zhuǎn)。

圖片
直接分區(qū):直接偏置不需要內(nèi)核空間作為中轉(zhuǎn)副本數(shù)據(jù),或者直接在物理內(nèi)存申請(qǐng)一塊空間,這塊空間映射到內(nèi)核地址空間和用戶地址空間,應(yīng)用程序與磁盤之間數(shù)據(jù)的訪問通過這塊直接申請(qǐng)的物理內(nèi)存進(jìn)行交互。

圖片
既然直接縮短那么快,我們?yōu)槭裁床欢加弥苯佣⒛啬???shí)際上直接切斷有以下的缺點(diǎn)。直接截?cái)嗟娜秉c(diǎn):
不安全 消耗更多,因?yàn)樗皇窃贘VM中直接開辟空間。這部分內(nèi)存的回收只能依賴于垃圾回收機(jī)制,垃圾什么時(shí)候回收不受我們控制。 數(shù)據(jù)寫入物理內(nèi)存緩沖區(qū)中,程序就丟失了對(duì)這些數(shù)據(jù)的管理,即什么時(shí)候這些數(shù)據(jù)被最終寫入從磁盤只能由操作系統(tǒng)來決定,應(yīng)用程序無法重新插入。
綜上所述,所以我們使用
transferTo方法就是直接開辟了一段直接短路。所以性能相對(duì)而言提高了很多
使用內(nèi)存映射文件
NIO中新出的另一個(gè)特性就是內(nèi)存映射文件,內(nèi)存映射文件為什么速度快呢?其實(shí)原因和上面所講的一樣,也是在內(nèi)存中開辟了一段直接分開。與數(shù)據(jù)直接作交互。
//Version?4?使用Map映射文件
public?static?void?zipFileMap()?{
????//開始時(shí)間
????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));
????????????//內(nèi)存中的映射文件
????????????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的速度差不多的。
使用管道
Java NIO管道是2個(gè)線程之間的單向數(shù)據(jù)連接。Pipe有一個(gè)源通道和一個(gè)接收器通道。其中的源通道用于讀取數(shù)據(jù),接收器用于寫入數(shù)據(jù)。如果沒有數(shù)據(jù)偏移,讀線程也會(huì)重疊至寫入線程寫入數(shù)據(jù)。直到通道關(guān)閉。
?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();
????????//異步任務(wù)
????????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);
}
//異步任務(wù)
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();
????}
}
總結(jié)
生活處處都需要學(xué)習(xí),有時(shí)候只是一個(gè)簡單的優(yōu)化,可以讓你深入學(xué)習(xí)到各種不同的知識(shí)。所以在學(xué)習(xí)中要不求甚解,完全要知道這個(gè)知識(shí)也要了解為什么要這么做。 知行合一:學(xué)習(xí)完一個(gè)知識(shí)要努力應(yīng)用一遍。這樣才能記得牢靠。
源碼地址:https://github.com/modouxiansheng/Doraemon
參考文章
https://www.jianshu.com/p/f90866dcbffc https://juejin.im/post/5af942c6f265da0b7026050c 趣談Linux操作系統(tǒng) JAVA NIO直接透視和非直接重疊
自營滬牌代拍業(yè)務(wù),無中間商賺差價(jià),點(diǎn)擊直達(dá)
往期推薦
掃一掃,關(guān)注我
一起學(xué)習(xí),一起進(jìn)步
每周贈(zèng)書,福利不斷
﹀
﹀
﹀
深度內(nèi)容
推薦加入
最近熱門內(nèi)容回顧? ?#技術(shù)人系列

