<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

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

          共 7837字,需瀏覽 16分鐘

           ·

          2020-08-30 23:45


          轉(zhuǎn)自:不學(xué)無數(shù)的程序員

          鏈接:jianshu.com/p/25b328753017

          有一個需求需要將前端傳過來的10張照片,然后后端進(jìn)行處理以后壓縮成一個壓縮包通過網(wǎng)絡(luò)流傳輸出去。之前沒有接觸過用 Java 壓縮文件的,所以就直接上網(wǎng)找了一個例子改了一下用了,改完以后也能使用,但是隨著前端所傳圖片的大小越來越大的時候,耗費的時間也在急劇增加,最后測了一下壓縮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大小的圖片,并且循環(huán)十次進(jìn)行測試。打印的結(jié)果如下,時間大概是30秒。


          fileSize:20Mconsum?time:29599


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


          進(jìn)行優(yōu)化首先想到的是利用緩沖區(qū) BufferInputStream。在FileInputStream 中 read() 方法每次只讀取一個字節(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;


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


          BufferedInputStream 內(nèi)部封裝了一個byte數(shù)組用于存放數(shù)據(jù),默認(rèn)大小是 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)) {        //開始時間        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 效率已經(jīng)提升了許多了。


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


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


          使用 Channel


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


          在 NIO 中能夠產(chǎn)生 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 進(jìn)行數(shù)據(jù)傳輸,而是使用了 transferTo 的方法。這個方法是將兩個通道進(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.?


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


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


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


          ------ChannelfileSize:20Mconsum?time:1416


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


          那么為什么從內(nèi)核空間轉(zhuǎn)向用戶空間這段過程會慢呢?首先我們需了解的是什么是內(nèi)核空間和用戶空間。在常用的操作系統(tǒng)中為了保護(hù)系統(tǒng)中的核心資源,于是將系統(tǒng)設(shè)計為四個區(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應(yīng)用程序(應(yīng)用程序是都屬于用戶態(tài)的)需要訪問核心資源怎么辦呢?那就需要調(diào)用內(nèi)核中所暴露出的接口用以調(diào)用,稱之為系統(tǒng)調(diào)用。例如此時我們應(yīng)用程序需要訪問磁盤上的文件。此時應(yīng)用程序就會調(diào)用系統(tǒng)調(diào)用的接口 open() 方法,然后內(nèi)核去訪問磁盤中的文件,將文件內(nèi)容返回給應(yīng)用程序。大致的流程如下:


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


          既然我們要讀取一個磁盤的文件,要廢這么大的周折。有沒有什么簡單的方法能夠使我們的應(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)存申請一塊空間,這塊空間映射到內(nèi)核地址空間和用戶地址空間,應(yīng)用程序與磁盤之間數(shù)據(jù)的存取通過這塊直接申請的物理內(nèi)存進(jìn)行交互。



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


          1. 不安全。

          2. 消耗更多,因為它不是在 JVM 中直接開辟空間。這部分內(nèi)存的回收只能依賴于垃圾回收機(jī)制,垃圾什么時候回收不受我們控制。

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


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


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


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


          //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));
          //內(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(); }}


          打印如下:


          ---------MapfileSize:20Mconsum?time:1305


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


          使用 Pipe


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


           Whether or not a thread writing bytes to a pipe will block until another?thread?reads?those?bytes



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


          //Version 5 使用Pippublic?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í),有時候只是一個簡單的優(yōu)化,可以讓你深入學(xué)習(xí)到各種不同的知識。所以在學(xué)習(xí)中要不求甚解,不僅要知道這個知識也要了解為什么要這么做。

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


          源碼地址:https://github.com/modouxiansheng/Doraemon


          參考文章


          • https://www.jianshu.com/p/f90866dcbffc

          • https://juejin.im/post/5af942c6f265da0b7026050c

          • 趣談Linux操作系統(tǒng)?https://time.geekbang.org/column/article/90109

          • JAVA NIO 直接緩沖區(qū)和非直接緩沖區(qū)?https://my.oschina.net/happyBKs/blog/1592329

          瀏覽 28
          點贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  老牛AV国产性久久 | 中文字幕在线观看不卡 | 国产麻豆精品成人毛片 | 蜜桃人妻系列 | 亚洲成a 亚洲看逼 |