为了账号安全,请及时绑定邮箱和手机立即绑定

初识 Java NIO

标签:
Java

一、前言

也许你见过下面这样一段代码。

            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 NIO

好吧Java NIO 是 Java New IO。是 JDK 1.4 开始提供的一套新的可用来代替原 Java IO 的接口。然而这么多年过去了结果并木有。

图片描述

这里看到了 Java NIO 中的核心概念ChannelBuffer 以及 selector。关于 Java NIO 的更详细的说明可参考

三、原理探索

不管是 NIO 还是 IO都需要 new 一个 File***Stream 或者 RandomAccessFile 从而获取它的 FileChannel。而在这之前我们需要弄明白一些事情。当我们 new 一个流对象时究竟发生了什么与之密切相关的 FileDescriptor 又是什么它与 Channel 之间有着怎么样的联系

3.1 探究 FileDescriptor

这里先看一个简单的类图在心里有一个简单的地图。
图片描述

这里为了简单起见以 new 一个 FileInputStream 为例。

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

3.3 探究 Buffer

先来看一看 Buffer 的类图结构。
图片描述

Buffer 确实就是缓冲区上图中顶级父类 Buffer 下可以看成左边 ByteBuffer 和右边其他类型的 Buffer。其实只存在 ByteBuffer其他类型 Buffer 都是为了方便操作而言的。而 ByteBuffer 从内存的角度来看又分为 HeapByteBuffer 和 DirectedByteBuffer详细如下图。
图片描述

这里可能需要注意一下的是在 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    }

初始化完成后状态如下。
图片描述

  1. 读取 512 个字节到 Buffer
fileChannel.read(byteBuffer);

将数据写入到了 Buffer 后Buffer 的状态如下。
图片描述

  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    }

下图是这段代码主要做的事情。
图片描述

四、总结

关于 Java NIO 的探索就先到这里了其本身的实现还是较为复杂的。尤其是对于非阻塞的实现功力实在尚浅暂时没有分析的很清楚。

最后感谢你能读到此文章。如果我的分享对你有帮忙还请帮忙点个赞。谢谢。

点击查看更多内容
1人点赞

若觉得本文不错,就分享一下吧!

评论

作者其他优质文章

正在加载中
移动开发工程师
手记
粉丝
4
获赞与收藏
29

关注作者,订阅最新文章

阅读免费教程

感谢您的支持,我会继续努力的~
扫码打赏,你说多少就多少
赞赏金额会直接到老师账户
支付方式
打开微信扫一扫,即可进行扫码打赏哦
今天注册有机会得

100积分直接送

付费专栏免费学

大额优惠券免费领

立即参与 放弃机会
意见反馈 帮助中心 APP下载
官方微信

举报

0/150
提交
取消