2021-2-19:請問你知道 Java 如何高效能操作檔案麼?

乾貨滿滿張雜湊發表於2021-02-19

一般高效能的涉及到儲存框架,例如 RocketMQ,Kafka 這種訊息佇列,儲存日誌的時候,都是通過 Java File MMAP 實現的,那麼什麼是 Java File MMAP 呢?

什麼是 Java File MMAP

儘管從JDK 1.4版本開始,Java 記憶體對映檔案(Memory Mapped Files)就已經在java.nio包中,但它對很多程式開發者來說仍然是一個相當新的概念。引入 NIO 後,Java IO 已經相當快,而且記憶體對映檔案提供了 Java 有可能達到的最快 IO 操作,這也是為什麼那些高效能 Java 應用應該使用記憶體對映檔案來持久化資料。
作為 NIO 的一個重要的功能,MMAP 方法為我們提供了將檔案的部分或全部對映到記憶體地址空間的能力,同當這塊記憶體區域被寫入資料之後會變成髒頁,作業系統會用一定的演算法把這些資料寫入到檔案中,而我們的 Java 程式不需要去關心這些。這就是記憶體對映檔案的一個關鍵優勢,即使你的程式在剛剛寫入記憶體後就掛了,作業系統仍然會將記憶體中的資料寫入檔案系統。
另外一個更突出的優勢是共享記憶體,記憶體對映檔案可以被多個程式同時訪問,起到一種低時延共享記憶體的作用。

Java File MMAP 與直接操作檔案效能對比

package com.github.hashZhang.scanfold.jdk.file;
import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;
import java.util.Random;

public class FileMmapTest {
    public static void main(String[] args) throws Exception {
        //記錄開始時間
        long start = System.currentTimeMillis();
        //通過RandomAccessFile的方式獲取檔案的Channel,這種方式針對隨機讀寫的檔案較為常用,我們用檔案一般是隨機讀寫
        RandomAccessFile randomAccessFile = new RandomAccessFile("./FileMmapTest.txt", "rw");
        FileChannel channel = randomAccessFile.getChannel();
        System.out.println("FileChannel初始化時間:" + (System.currentTimeMillis() - start) + "ms");

        //記憶體對映檔案,模式是READ_WRITE,如果檔案不存在,就會被建立
        MappedByteBuffer mappedByteBuffer1 = channel.map(FileChannel.MapMode.READ_WRITE, 0, 128 * 1024 * 1024);
        MappedByteBuffer mappedByteBuffer2 = channel.map(FileChannel.MapMode.READ_WRITE, 0, 128 * 1024 * 1024);

        System.out.println("MMAPFile初始化時間:" + (System.currentTimeMillis() - start) + "ms");

        start = System.currentTimeMillis();
        testFileChannelSequentialRW(channel);
        System.out.println("FileChannel順序讀寫時間:" + (System.currentTimeMillis() - start) + "ms");

        start = System.currentTimeMillis();
        testFileMMapSequentialRW(mappedByteBuffer1, mappedByteBuffer2);
        System.out.println("MMAPFile順序讀寫時間:" + (System.currentTimeMillis() - start) + "ms");

        start = System.currentTimeMillis();
        try {
            testFileChannelRandomRW(channel);
            System.out.println("FileChannel隨機讀寫時間:" + (System.currentTimeMillis() - start) + "ms");
        } finally {
            randomAccessFile.close();
        }

        //檔案關閉不影響MMAP寫入和讀取
        start = System.currentTimeMillis();
        testFileMMapRandomRW(mappedByteBuffer1, mappedByteBuffer2);
        System.out.println("MMAPFile隨機讀寫時間:" + (System.currentTimeMillis() - start) + "ms");
    }


    public static void testFileChannelSequentialRW(FileChannel fileChannel) throws Exception {
            byte[] bytes = "測試字串1測試字串1測試字串1測試字串1測試字串1測試字串1測試字串1測試字串1測試字串1測試字串1測試字串1".getBytes();
            byte[] to = new byte[bytes.length];
            //分配直接記憶體,減少複製
            ByteBuffer byteBuffer = ByteBuffer.allocateDirect(bytes.length);
            //順序寫入
            for (int i = 0; i < 100000; i++) {
                byteBuffer.put(bytes);
                byteBuffer.flip();
                fileChannel.write(byteBuffer);
                byteBuffer.flip();
            }

            fileChannel.position(0);
            //順序讀取
            for (int i = 0; i < 100000; i++) {
                fileChannel.read(byteBuffer);
                byteBuffer.flip();
                byteBuffer.get(to);
                byteBuffer.flip();
            }
    }

    public static void testFileMMapSequentialRW(MappedByteBuffer mappedByteBuffer1, MappedByteBuffer mappedByteBuffer2) throws Exception {
        byte[] bytes = "測試字串2測試字串2測試字串2測試字串2測試字串2測試字串2測試字串2測試字串2測試字串2測試字串2測試字串2".getBytes();
        byte[] to = new byte[bytes.length];

        //順序寫入
        for (int i = 0; i < 100000; i++) {
            mappedByteBuffer1.put(bytes);
        }
        //順序讀取
        for (int i = 0; i < 100000; i++) {
            mappedByteBuffer2.get(to);
        }
    }

    public static void testFileChannelRandomRW(FileChannel fileChannel) throws Exception {
        try {
            byte[] bytes = "測試字串1測試字串1測試字串1測試字串1測試字串1測試字串1測試字串1測試字串1測試字串1測試字串1測試字串1".getBytes();
            byte[] to = new byte[bytes.length];
            //分配直接記憶體,減少複製
            ByteBuffer byteBuffer = ByteBuffer.allocateDirect(bytes.length);
            //隨機寫入
            for (int i = 0; i < 100000; i++) {
                byteBuffer.put(bytes);
                byteBuffer.flip();
                fileChannel.position(new Random(i).nextInt(bytes.length*100000));
                fileChannel.write(byteBuffer);
                byteBuffer.flip();
            }
            //隨機讀取
            for (int i = 0; i < 100000; i++) {
                fileChannel.position(new Random(i).nextInt(bytes.length*100000));
                fileChannel.read(byteBuffer);
                byteBuffer.flip();
                byteBuffer.get(to);
                byteBuffer.flip();
            }
        } finally {
            fileChannel.close();
        }
    }

    public static void testFileMMapRandomRW(MappedByteBuffer mappedByteBuffer1, MappedByteBuffer mappedByteBuffer2) throws Exception {
        byte[] bytes = "測試字串2測試字串2測試字串2測試字串2測試字串2測試字串2測試字串2測試字串2測試字串2測試字串2測試字串2".getBytes();
        byte[] to = new byte[bytes.length];

        //隨機寫入
        for (int i = 0; i < 100000; i++) {
            mappedByteBuffer1.position(new Random(i).nextInt(bytes.length*100000));
            mappedByteBuffer1.put(bytes);
        }
        //隨機讀取
        for (int i = 0; i < 100000; i++) {
            mappedByteBuffer2.position(new Random(i).nextInt(bytes.length*100000));
            mappedByteBuffer2.get(to);
        }
    }
}

在這裡,我們初始化了一個檔案,並把它對映到了128M的記憶體中。分FileChannel還有MMAP的方式,通過順序或隨機讀寫,寫了一些內容並讀取一部分內容。

執行結果是:

FileChannel初始化時間:7ms
MMAPFile初始化時間:8ms
FileChannel順序讀寫時間:420ms
MMAPFile順序讀寫時間:20ms
FileChannel隨機讀寫時間:860ms
MMAPFile隨機讀寫時間:45ms

可以看到,通過MMAP記憶體對映檔案的方式操作檔案,更加快速,並且效能提升的相當明顯。

微信搜尋“我的程式設計喵”關注公眾號,每日一刷,輕鬆提升技術,斬獲各種offer

image

相關文章