初識 Java NIO

1、前言

也許你見過下面這樣一段代碼。html

File file = new File("file-map-sample.txt");
            file.delete();
            file.createNewFile();

            RandomAccessFile randomAccessFile = new RandomAccessFile(file,"rw");

            FileChannel fileChannel = randomAccessFile.getChannel();
            MappedByteBuffer mappedByteBuffer = fileChannel.map(FileChannel.MapMode.READ_WRITE,0,Integer.MAX_VALUE);

            System.out.println("MappedByteBuffer capacity " + mappedByteBuffer.capacity());

            long currentTime = System.currentTimeMillis();
            int size = Integer.MAX_VALUE / 4;
            for (int i = 0; i < size; i++) {
                mappedByteBuffer.putInt(i);
            }
            mappedByteBuffer.force();
            fileChannel.close();
            randomAccessFile.close();
            System.out.println("MappedByteBuffer Write " + (System.currentTimeMillis() - currentTime) + " ms");

複製代碼

經過 Java NIO 中的文件映射進行寫文件。關於 NIO 大部分同窗應該知道有這麼個東西,但好像又不怎麼熟悉,由於平時要用到的地方可能真的不太多吧。java

2、關於 Java NIO

好吧,Java NIO 是 Java New IO。是 JDK 1.4 開始提供的一套新的可用來代替原 Java IO 的接口。然而這麼多年過去了,結果並木有。android

Java NIO.jpg

這裏看到了 Java NIO 中的核心概念:Channel,Buffer 以及 selector。關於 Java NIO 的更詳細的說明,可參考c#

  1. 英文原文 tutorials.jenkov.com/java-nio/ov…
  2. 中文翻譯 ifeve.com/java-nio-al…

3、原理探索

不論是 NIO 仍是 IO,都須要 new 一個 File***Stream 或者 RandomAccessFile 從而獲取它的 FileChannel。而在這以前,咱們須要弄明白一些事情。當咱們 new 一個流對象時究竟發生了什麼?與之密切相關的 FileDescriptor 又是什麼?它與 Channel 之間有着怎麼樣的聯繫?bash

3.1 探究 FileDescriptor

這裏先看一個簡單的類圖,在內心有一個簡單的地圖。 app

FileChannel.jpg

這裏爲了簡單起見,以 new 一個 FileInputStream 爲例。dom

public FileInputStream(File file) throws FileNotFoundException {
		   ......
154        fd = new FileDescriptor();
155
           ......
165        open(name);
166
		   ......
169    }
複製代碼

去掉校驗和 BlockGuard 相關的代碼,FileInputStream 的構造方法簡化下來還有 2 個步驟,new 一個 FileDescriptor 對象 和 open() 文件。先來看看 FileDescriptor。函數

public /**/ FileDescriptor() {
62        descriptor = -1;
63    }
複製代碼

默認爲 -1,這個是虛晃一槍。確定得有地方給它真正的值。我想,應該是 open() 裏面。不過 open() 是調用的 native 方法 open0()。因此須要進一步看 open0() 的實現。這裏須要看到 FileInputStream 的 native 代碼 FileInputStream.c 中對於 open0 的實現。源碼分析

66 FileInputStream_open0(JNIEnv *env, jobject this, jstring path) {
67    fileOpen(env, this, path, fis_fd, O_RDONLY);
68}
複製代碼

open0() 進一步調用了函數 fileOpen()。注意這裏的第 4 個參數 fis_fd。它是 Java 層 fd 在 native 層的 fieldId。能夠看看它的定義和初始化,就會一目瞭然了。性能

jfieldID fis_fd; /* id for jobject 'fd' in java.io.FileInputStream */

60static void FileInputStream_initIDs(JNIEnv *env) {
61    jclass clazz = (*env)->FindClass(env, "java/io/FileInputStream");
62    fis_fd = (*env)->GetFieldID(env, clazz, "fd", "Ljava/io/FileDescriptor;");
63}
複製代碼

接着繼續看 fileOpen() 函數,它在 io_util_md.c 中定義。

88void
89fileOpen(JNIEnv *env, jobject this, jstring path, jfieldID fid, int flags)
90{
91    WITH_PLATFORM_STRING(env, path, ps) {
92        FD fd;
93
           ......
100        fd = handleOpen(ps, flags, 0666);
101        if (fd != -1) {
102            SET_FD(this, fd, fid);
103        } else {
104            throwFileNotFoundException(env, path);
105        }
106    } END_PLATFORM_STRING(env, ps);
107}
複製代碼

這裏看到了 FD 的定義,不過它只不過是一個宏定義而已,原型就是 jint。那這個函數所作的事情就是打開文件得到 fd,而後經過宏定義 SET_FD 賦值給 Java 層的 fd 對象中的 descriptor。對,這是個結論,咱們來看看具體的實現過程。先看 handleOpen()。

65 FD
66 handleOpen(const char *path, int oflag, int mode) {
67    FD fd;
68    RESTARTABLE(open64(path, oflag, mode), fd);
      ......
84    return fd;
85}
複製代碼

open64() 是一個宏定義,指向 open() 函數。RESTARTABLE 也是一個宏定義,其就是將前面的參數結果賦值給後面的參數。那麼,這裏就是將 open() 函數的返回結果文件描述符 FD 賦值給 fd。

經過上述 handleOpen() 就打開了文件,而且返回了文件的描述符,而若是文件描述符爲 -1 的話那就會拋出著名的 exception —— FileNotFoundException。而後再來看看

49#define SET_FD(this, fd, fid) \
50    if ((*env)->GetObjectField(env, (this), (fid)) != NULL) \
51        (*env)->SetIntField(env, (*env)->GetObjectField(env, (this), (fid)),IO_fd_fdID, (fd))
複製代碼

這裏的 (*env)->GetObjectField(env, (this), (fid)) 就是獲取 FileInputStream 的 fd 屬性,而 IO_fd_fdID 就是其屬性的屬性 descriptor,代碼以下。

IO_fd_fdID = (*env)->GetFieldID(env, fdClass, "descriptor", "I");
複製代碼

至此,就分析完了文件的打開與文件描述符 FD 了。當咱們 new 一個 FileInputStream 的時候,其實底層是調用了函數 open(),而且返回了一個文件描述符 fd,然後對文件的全部操做其實都是做用在這個 fd 之上的。

3.2 探究 FileChannel

在 new 完 FileInputStream 後,能夠經過其 getChannel() 方法得到一個 FileChannel 對象。從上面的類圖中可知,FileChannel 是一個抽象類,真正的實現類在 FileChannelImpl。FileChannelImpl 中有 2 個核心屬性分別是 fd 和 nd。fd 好理解,就是 FileDescriptor。而 nd 是 FileDispatcherImpl,字面意思 「文件分發」?仍是一塊兒來看看吧。再回到 FileInputStream.getChannel() 看看是如何得到 FileChannel 的。

456    public FileChannel getChannel() {
457        synchronized (this) {
458            if (channel == null) {
459                channel = FileChannelImpl.open(fd, path, true, false, this);
460            }
461            return channel;
462        }
463    }
複製代碼

FileChannelImpl 的構造函數是私有的,只能經過其靜態方法 open() 來構造,而這裏傳入的參數依次是文件描述符 fd,路徑,可讀,可寫(inputstream 不可寫),FileInputStream。在 open() 方法中,就是直接 new 一個 FileChannelImple 對象。那來看看它的構造方法。

98    private FileChannelImpl(FileDescriptor fd, String path, boolean readable,
99                            boolean writable, boolean append, Object parent)
100    {
101        this.fd = fd;
102        this.readable = readable;
103        this.writable = writable;
104        this.append = append;
105        this.parent = parent;
106        this.path = path;
107        this.nd = new FileDispatcherImpl(append);
112    }
複製代碼

前面幾個屬性都是基本的賦值操做,主要須要進一步分析 FileDispatcherImpl。

43    FileDispatcherImpl(boolean append) {
44        /* append is ignored */
45    }
複製代碼

呃,什麼都沒有,......

看到這裏,就有點懵了,仍是沒明白 FileChannel 是個什麼東西。不過仍是能夠總結下就是,其有兩個核心的屬性 fd 和 nd,看起來 FileChannel 對 Buffer 的讀寫操做應該是經過 nd 來實現的,nd 操做的也必將是 fd 。

前面有說過 Channel 是 NIO 的核心之一,那除了 FileChannel,還有......看看類圖吧。

Channel.jpg

3.3 探究 Buffer

先來看一看 Buffer 的類圖結構。

Buffer.jpg

Buffer 確實就是緩衝區,上圖中,頂級父類 Buffer 下能夠當作左邊 ByteBuffer 和右邊其餘類型的 Buffer。其實只存在 ByteBuffer,其餘類型 Buffer 都是爲了方便操做而言的。而 ByteBuffer 從內存的角度來看又分爲 HeapByteBuffer 和 DirectedByteBuffer,詳細以下圖。

ByteBuffer.jpg

這裏可能須要注意一下的是,在 Android 中和在 Java 中,它們的實現是有差別的。另外,若是以前有熟悉的 okio 的同窗,看到這裏應該更加不會陌生。固然,你如今也能夠去看一看,okio 也是充分運用了緩衝來讀寫數據,以提升IO性能的。Okio深刻分析—源碼分析部分

3.4 從 Channel 讀數據到 Buffer

  1. Buffer 的初始化 這裏假設咱們是直接從 Java 堆內存分配 Buffer 的空間,也就是咱們是經過 ByteBuffer.allocate(1024) 初始化的 Buffer。這裏仍是看一看代碼,有個印象。
278    public static ByteBuffer allocate(int capacity) {
279        if (capacity < 0)
280            throw new IllegalArgumentException();
281        return new HeapByteBuffer(capacity, capacity);
282    }

53    private HeapByteBuffer(int cap, int lim, boolean isReadOnly) {
54        super(-1, 0, lim, cap, new byte[cap], 0);
55        this.isReadOnly = isReadOnly;
56    }
複製代碼

初始化完成後,狀態以下。

Buffer 初始化.jpg

  1. 讀取 512 個字節到 Buffer
fileChannel.read(byteBuffer);
複製代碼

將數據寫入到了 Buffer 後,Buffer 的狀態以下。

Buffer 寫入512.jpg

  1. read() 方法的實現
181    public int read(ByteBuffer dst) throws IOException {
182        ensureOpen();
183        if (!readable)
184            throw new NonReadableChannelException();
185        synchronized (positionLock) {
186            int n = 0;
187            int ti = -1;
188            try {
189                begin();
190                ti = threads.add();
191                if (!isOpen())
192                    return 0;
193                do {
194                    n = IOUtil.read(fd, dst, -1, nd);
195                } while ((n == IOStatus.INTERRUPTED) && isOpen());
196                return IOStatus.normalize(n);
197            } finally {
198                threads.remove(ti);
199                end(n > 0);
200                assert IOStatus.check(n);
201            }
202        }
203    }
複製代碼

下圖是這段代碼主要作的事情。

Read Buffer.jpg

4、總結

關於 Java NIO 的探索就先到這裏了,其自己的實現仍是較爲複雜的。尤爲是對於非阻塞的實現,功力實在尚淺暫時沒有分析的很清楚。

最後,感謝你能讀到此文章。若是個人分享對你有幫忙,還請幫忙點個贊。謝謝。

相關文章
相關標籤/搜索