2021-2-19:請問你知道 Java 如何高性能操做文件麼?

通常高性能的涉及到存儲框架,例如 RocketMQ,Kafka 這種消息隊列,存儲日誌的時候,都是經過 Java File MMAP 實現的,那麼什麼是 Java File MMAP 呢?java

什麼是 Java File MMAP

儘管從JDK 1.4版本開始,Java 內存映射文件(Memory Mapped Files)就已經在java.nio包中,但它對不少程序開發者來講仍然是一個至關新的概念。引入 NIO 後,Java IO 已經至關快,並且內存映射文件提供了 Java 有可能達到的最快 IO 操做,這也是爲何那些高性能 Java 應用應該使用內存映射文件來持久化數據。
做爲 NIO 的一個重要的功能,MMAP 方法爲咱們提供了將文件的部分或所有映射到內存地址空間的能力,同當這塊內存區域被寫入數據以後會變成髒頁,操做系統會用必定的算法把這些數據寫入到文件中,而咱們的 Java 程序不須要去關心這些。這就是內存映射文件的一個關鍵優點,即便你的程序在剛剛寫入內存後就掛了,操做系統仍然會將內存中的數據寫入文件系統。
另一個更突出的優點是共享內存,內存映射文件能夠被多個進程同時訪問,起到一種低時延共享內存的做用。git

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的方式,經過順序或隨機讀寫,寫了一些內容並讀取一部份內容。github

運行結果是:算法

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

能夠看到,經過MMAP內存映射文件的方式操做文件,更加快速,而且性能提高的至關明顯。編程

微信搜索「個人編程喵」關注公衆號,每日一刷,輕鬆提高技術,斬獲各類offer微信

image

相關文章
相關標籤/搜索