一起聊聊Java NIO

一、IO的基本概念

IO我的理解就是数据的发送与接收。

IO操作场景一般分为:1.文件IO   2.网络IO(我们平时讲的BIO、NIO、AIO其实说的都是网络编程IO模式)

在jdk 1.4中引入了新的java I/O库 java.nio.*包  其目的是为了提高速度,新的I/O库也就是NIO,有的人翻译成 no-blocking io有的人翻译成 new io,其实都一样。在Java API中提供了两套NIO,一套是针对标准输入输出NIO,另一套就是网络编程NIO

二、IO和NIO的区别

IO NIO
面向流(Stream) 面向缓冲区(Buffer)
阻塞IO(Blocking IO) 非阻塞IO(Non Blocking IO)
选择器(Selector)

IO和NIO最大的区别就是,IO是面向流的,NIO是面向缓冲区的,面向流的IO每次只能从流中读取一个或多个字节,直到读取所有字节。而NIO是把数据读取到缓冲区中需要时可在缓冲区前后移动。具体的区别在后面分析NIO的三大核心1.Buffer缓冲区 2.Channel通道 3.Selector选择器,都会讲到。这里说一下对于标准的输入输出IO(文件IO),已经使用nio进行优化过了。所以对文件IO的操作IO和NIO操作效率没有什么差异。

三、NIO的三大核心

Buffer缓冲区、Channel通道、Selector选择器,Channel负责传输,Buffer负责存储,Selector负责监听。

Buffer 缓冲区

首先看下java.nio.Buffer 这个抽象类

一起聊聊Java NIO_第1张图片

缓冲区Buffer负责Java NIO中数据的存取,就是写数据到缓冲区/读取缓冲区中的数据,所以缓冲区的核心方法就是get()和put()。以我们常用的ByteBuffer的例:

一起聊聊Java NIO_第2张图片

一起聊聊Java NIO_第3张图片

Buffer类维护了4个核心的变量来对缓冲区(数组)进行操作

1.capacity 容量

缓冲区能容纳的最大数据容量,容量在缓冲区创建是设定,且不能改变。(原因底层就是缓冲区就是数组实现的)

2.limit 界限

表示缓冲区可以操作的大小,也是缓冲区数据的总数

3.position 位置

下一个要被读或写的位置,position会自动由相应的get() 和put() 函数更新

4.mark 标记

用于做标记使用,当Buffer调用mark()时,会生成一个标记,当position等发生变化时,可通过remark()恢复之前的Buffer状态

0 <= mark <= position <= limit <= capacity

Buffer代码演示

    public static void main(String[] args) {
        // 初始化一个长度为64的ByteBuffer
        ByteBuffer byteBuffer = ByteBuffer.allocate(8);
        // 打印4个核心参数情况
        System.out.println("初始化时 Limit " + byteBuffer.limit());
        System.out.println("初始化时 position " + byteBuffer.position());
        System.out.println("初始化时 capacity " + byteBuffer.capacity());
        System.out.println("初始化时 mark " + byteBuffer.mark());
        System.out.println("------------------------------------------");
        // 创建一个字符串 哈哈
        String name = "haha";
        // 使用Buffer的put方法
        byteBuffer.put(name.getBytes());

        // 再次打印4个核心参数情况
        System.out.println("put之后 Limit " + byteBuffer.limit());
        System.out.println("put之后 position " + byteBuffer.position());
        System.out.println("put之后 capacity " + byteBuffer.capacity());
        System.out.println("put之后 mark " + byteBuffer.mark());
        System.out.println("------------------------------------------");

        // 若要读取数据,需要使用提供的flip()切换读模式
        byteBuffer.flip();

        // 再次打印4个核心参数情况
        System.out.println("flip之后 Limit " + byteBuffer.limit());
        System.out.println("flip之后 position " + byteBuffer.position());
        System.out.println("flip之后 capacity " + byteBuffer.capacity());
        System.out.println("flip之后 mark " + byteBuffer.mark());
        System.out.println("------------------------------------------");

        // 申请一个存储读数据的空间,空间大小使用Buffer的元素最大值limit
        byte[] bytes = new byte[byteBuffer.limit()];
        // 执行get操作 读取数据
        byteBuffer.get(bytes);

        // 再次打印4个核心参数情况
        System.out.println("get之后 Limit " + byteBuffer.limit());
        System.out.println("get之后 position " + byteBuffer.position());
        System.out.println("get之后 capacity " + byteBuffer.capacity());
        System.out.println("get之后 mark " + byteBuffer.mark());
        System.out.println("------------------------------------------");

        // 输出数据小老弟
        System.out.println("get 得到的结果:" + new String(bytes));
        System.out.println("------------------------------------------");

        // 再次写数据就要清空缓冲区 需要使用提供的clear()方法
        byteBuffer.clear();

        System.out.println("clear之后 Limit " + byteBuffer.limit());
        System.out.println("clear之后 position " + byteBuffer.position());
        System.out.println("clear之后 capacity " + byteBuffer.capacity());
        System.out.println("clear之后 mark " + byteBuffer.mark());
    }

运行结果

一起聊聊Java NIO_第4张图片

我们可以看到Buffer(缓冲区)初始化时容量设置的64固定值,因此capacity(容量)和limit(界限)等于8,界限等于8因为当前为初始化后的Buffer为写入模式,所以limit(界限)等于8,也就是最多写入8个元素,又因为当前Buffer(缓冲区)没有数据所以position(位置)等于0

当调用put()函数将字符串"haha"存入缓冲区,此时position(位置)变成了4,capacity(容量)和limit(界限)没有发生变化。

关键点来了,当我们put()操作后想要读取Buffer(缓冲区)数据,此时我们必须切换到读模式,也就是调用flip()函数。调用flip()后我们会发现limit=4变成了position的值,position=0,这个也很好理解写模式下的position是当前最后一个元素的位置,当切换读模式后,limit(界限)肯定就换成了position,也就是可读元素的最大个数,position置为0就是读数据肯定第一个元素开始读。

当调用get()函数后,position=4也就是数据读取到最后一个元素的位置了。

此时如果我们还想写数据就需要调用clear()清空缓冲区,此时的清理并非真正的清理只是重置了position、limit、capacity的值,数据并没有真正的清空。当有新数据写入时会发生覆盖。

put()操作、flip()操作、get()、clear()操作缓冲区图解

一起聊聊Java NIO_第5张图片

Channel 通道

channel前面也说了,他就是NIO中负责传输数据的。Channel相比IO中的Stream更加高效,可以异步双向传输,但是必须和buffer一起使用。

主要实现类

FileChannel,读写文件中的数据。
SocketChannel,通过TCP读写网络中的数据。
ServerSockectChannel,监听新进来的TCP连接,像Web服务器那样。对每一个新进来的连接都会创建一个SocketChannel。
DatagramChannel,通过UDP读写网络中的数据。

下面写一个用FileChannel实现图片复制的程序

    public static void main(String[] args) throws IOException {
        // 在 jdk1.7 之后提供了open方法获取channel
        FileChannel channel1 = FileChannel.open(Paths.get("/Users/gaoxing/Pictures/1.png"), StandardOpenOption.READ);

        FileChannel channel2 = FileChannel.open(Paths.get("/Users/gaoxing/Pictures/2.png"), StandardOpenOption.WRITE);

        // 将 2.png 复制给 新的 3.png
        ByteBuffer byteBuffer = ByteBuffer.allocate(1048576);

        // 将通道1数据读入缓冲区,此时缓冲区为写模式
        channel1.read(byteBuffer);
        // 切换缓冲区为读模式
        byteBuffer.flip();
        // 将缓冲区数据写入到通道2
        channel2.write(byteBuffer);
    }

不多比比了,就是运输数据的通道,文件IO正好这里也说到了,使用NIO做文件IO很方便几行代码就行,而且比IO更加高效。

Selector 选择器

说到Selector就要讲网络IO了,网络IO一般有BIO、NIO、AIO

  BIO NIO AIO
IO模型 同步阻塞 同步非阻塞 异步非阻塞
编码难度 简单 复杂 复杂
吞吐量

先说下I/O的过程,为了保证用户进程不能直接操作内核,保证内核空间安全,操作系统将虚拟空间划分为两部分,一部分是用户空间,一部分是内核空间。当我们要读取数据时要从内核空间找数据准备好,再复制到用户空间。这个过程是需要等待的。

BIO

一起聊聊Java NIO_第6张图片

Tomcat默认使用的就是BIO,也就是阻塞IO,阻塞IO也就是再I/O过程中一直等待,每来一个请求就创建一个线程去处理。

NIO:

一起聊聊Java NIO_第7张图片

1.首先Selector通过select()方法监听channel事件

2.当有一个客户端连接时,selector监听到一个连接事件,通过ServerSocketChannel 注册时绑定的selectionKey获取SocketChannel,将SocketChannel注册到Selector并监听Read事件

3.如果socketChannel已经准备好数据,此时select()监听到了Read事件,会去Channel中读取客户端发来的数据并处理。

总结:NIO模型中的Selector就相当于后宫太监总管,负责管理后宫每个妃子(客户端)的各种事件,例如:连接事件、读事件、写事件。NIO相对于BIO的非阻塞体现在BIO要一直等待读取客户端的数据,客户端写的慢写的时间长,他就得一直等着。而NIO把等待的事情交给了大总管Selector,Selector负责轮询所有已经注册的客户端,发现有读事件发生了,立刻交给后台线程处理,后台线程直接Channel中获取数据,因为数据时已经准备好的了,也就是非阻塞的。说白了,NIO就是一个Selector监听所有的Channel发现有事件发生了,把所有的事件全都拿过来然后交给后台线程处理,后台一个线程不用傻傻的等待一个客户端,就是来活了你就干,没活你就呆着老弟。好处就是一个线程可以处理更多的客户端,而不是像BIO那样一个线程只负责一个客户端。Redis就是典型的NIO线程模型,Selector搜集所有连接事件交给后台线程,后台连续执行所有命令。

适用场景

NIO适用于一些聊天、弹幕之类的系统,这种数据少连接多的场景NIO的多路复用机制才有用。

你可能感兴趣的:(java)