20、nio 类库

内容来自王争 Java 编程之美
Java NIO API

Java 中的 I / O 类库除了 java.io 之外,还包括 java.nio
既然已经有了 java.io了,为什么还要再开发一个新的 java.nio 呢?java.nio 跟 java.io 有何区别?在平时的开发中,什么时候使用 java.io?
什么时候使用 java.nio?面试中常被问到的 BIO、NIO、AIO 又是什么东西?带着这些问题,我们来学习本节的内容:java.nio

1、java.nio 类库

Buffer 影响编码(byte、long、直接缓冲区),Channel 影响来源(阻塞、非阻塞、异步)

java.nio 类库在 JDK 1.4 中引入,nio 的全称为 New I / O
不过,因为其相对于 java.io 来说,对 I / O 提供了非阻塞的访问方式(这个待会再讲),所以很多人也把 nio 解读为 Non-blocking I / O
除此之外,尽管从功能上 java.nio 可以完全替代 java.io,但在平时的开发中

  • 对于普通的文件读写,我们更倾向于使用简单的 java.io
  • java.nio 发挥作用的场合更多的是网络编程,所以还有人把 nio 解读为 Network I / O

上一节中讲到,在 java.io 中,Stream 是一个核心的概念,所有的 I / O 都抽象为 Stream,读写 Stream 就等同于读写 I / O
在 java.nio 中,已经没有了 Stream,转而引入了 Channel,Channel 类似 Stream,也是对 I / O 的抽象
除此之外,java.nio 还引入了一个新的概念:Buffer,用来存储待写入或读取的数据

我们先拿一个比较简单的文件读写的例子,来看一下 Channel 和 Buffer 是如何使用的,让你对 java.nio 有个最初步的直观的认识,示例代码如下所示

FileChannel channel = FileChannel.open(Paths.get("F:\\test-file\\in.txt"));
int len;
byte[] bytes = new byte[512];
ByteBuffer buffer = ByteBuffer.allocate(512); // 获取缓冲区

while ((len = channel.read(buffer)) != -1) {
    buffer.flip();             // 切换到读模式
    buffer.get(bytes, 0, len); // 获取缓冲区中的数据
    System.out.println(new String(bytes, 0, len));
    buffer.clear();            // 清空缓冲区
}

除了上面提到的 Buffer、Channel 之外,java.nio 中还有两个重要的概念:Selector 和 AsynchronousChannel,接下来,我们就详细介绍一下 java.nio 中的这 4 个核心概念

1.1、Buffer

Buffer 本质上就是一块内存,就相当于在使用 java.io 编程时申请的 byte 数组

常用到 Buffer 有:ByteBuffer、ShortBuffer、IntBuffer、LongBuffer、FloatBuffer、DoubleBuffer、CharBuffer、MappedByteBuffer
这些 Buffer 的不同之处在于:解析数据的方式不同,比如 CharBuffer 按照字符来解析数据,有点类似 java.io 中的字符流

上一节我们讲到,java.io 的设计有诸多问题,而 java.nio 的设计要优于 java.io
上一节讲到,java.io 分别为字符流和字节流设计了不同的类库,在代码实现上有些重复,毕竟 I / O 读写操作都是相同的,唯一的区别只是数据的解析方式不同:字节流类按照字节解析数据,字符流类按照字符解析数据
java.nio 将解析这部分功能抽取出来,独立到 Buffer 类中,不同的 Channel 跟不同的 Buffer 组合在一起,可以实现不同的 IO 读写需求

  • 将 FileChannel 跟 ByteBuffer 组合起来,就相当于 java.io 中的文件字节流类(FileInputStream、FileOutputStream)
  • 将 FileChannel 跟 CharBuffer 组合起来,就相当于 java.io 中的文件字符流类(FileReader、FileWriter)

实际上,Channel 和 Buffer 独立开发,组合起来使用,这种设计思路应用的就是面向对象中 "组合优于继承" 的设计思想,通过组合来替代继承,避免了继承带来的组合爆炸问题
正因如此,实现相同甚至更多功能的情况下,java.nio 中的类的个数却比 java.io 中的类的个数少
关于 "组合优于继承" 这一设计思想的详细介绍,你可以阅读我的《设计模式之美》这本书

1.2、Channel

常用的 Channel 有:FileChannel、DatagramChannel、SocketChannel、ServerSocketChannel

  • FileChannel 用于文件读写,DatagramChannel、SocketChannel、ServerSocketChannel 用于网络编程
  • DatagramChannel 用来读写 UDP 数据,SocketChannel 和 ServerSocketChannel 用来读写 TCP 数据
  • SocketChannel 和 ServerSocketChannel 的区别在于:ServerSocketChannel 用于服务器编程,可以使用 accept() 函数监听客户端(SocketChannel)的连接请求

java.nio 中的 Channel 既可以读,也可以写,而 java.io 中的 Stream 要么只能读,要么只能写,这也是 java.nio 比 java.io 类少的另一个重要原因
除此之外,Channel 的设计也利用了 "组合优于继承" 的设计思想,java.nio 中包含大量的 Channel 接口,每个接口定义了一种功能,每个 Channel 类通过实现不同的接口组合,来支持不同的功能组合
如下图所示,其中,FileChannel 实现了 3 个接口,支持 3 种不同的功能
image

Channel 有两种运行模式:阻塞模式和非阻塞模式

  • FileChannel 只支持阻塞模式
  • DatagramChannel、SocketChannel、ServerSocketChannel 支持阻塞和非阻塞两种模式,默认为阻塞模式
    我们可以调用 configureBlocking(false) 函数将其设置为非阻塞模式,非阻塞 Channel 一般会配合 Selector,用于实现多路复用 I / O 模型

那么,到底什么是阻塞模式?什么是非阻塞模式呢?

线程在调用 read() 或 write() 函数对 I / O 进行读写时,如果 I / O 不可读或者不可写(待会解释这两个的意思),那么

  • 在阻塞模式下:read() 或 write() 函数会等待,直到读取到数据或者写入完成时才会返回
  • 在非阻塞模式下:read() 或 write() 函数会直接返回,并报告读取或写入未成功

上一节,我们提到,在操作系统层面,主要的 I / O 有:文件、网络、标准输入输出、管道

  • 文件是没有非阻塞模式的,毕竟文件不存在不可读和不可写的情况
  • 网络、标准输入输出、管道都存在阻塞和非阻塞两种模式,我们拿最常用的网络来举例

一般来讲,应用程序调用 read() 或 write() 函数读取或写入数据,数据会在应用程序缓冲区、内核缓冲区、 I / O 设备这三者之间拷贝传递,如下图所示
关于这点,我们下一节详细介绍
image

当调用 read() 函数时,如果内核读缓冲区中没有数据可读,比如网络连接的对方此时并未发送数据过来,那么

  • 在阻塞模式下:read() 函数会等待,直到对方发送数据过来,内核读缓冲区中有数据可读时,才会将内核读缓冲区中的数据拷贝到应用程序缓存中,然后 read() 函数才返回
  • 在非阻塞模式下:read() 函数会直接返回,并报告读取情况

当调用 write() 函数时,如果内核写缓冲区中没有足够空间承载应用程序缓存中的数据,比如网络不好,原来的数据还没来得及发送出去,那么

  • 在阻塞模式下:write() 函数会等待,直到内核写缓冲区中有足够空间,应用程序缓冲区中的数据全部写入内核写缓冲区,write() 函数才会返回
  • 在非阻塞模式下:write() 函数会能写多少写多少,即便还有一部分未能写入内核写缓冲区,也不会等待,直接返回,并报告写入情况

实际上,除了 read() 和 write() 函数有阻塞和非阻塞这两种模式之外,ServerSocketChannel 中用于服务器接收客户端的连接的 accpet() 函数,也有阻塞和非阻塞两种模式

  • 在阻塞模式下:调用 accept() 函数会等待,直到有客户端连接到来才返回
  • 在非阻塞模式下:调用 accept() 函数,如果没有客户端连接到来,会直接返回

1.3、Selector

在网络编程中,使用非阻塞模式,线程需要通过 while 循环,不停轮询调用 read()、write()、accept() 函数,查看是否有数据可读、是否可写、是否有客户端连接到来

ServerSocketChannel serverChannel = ServerSocketChannel.open();
serverChannel.bind(new InetSocketAddress("127.0.0.1", 1192));
serverChannel.configureBlocking(false); // 设置为非阻塞模式, 直接返回
ByteBuffer buffer = ByteBuffer.allocate(1024);

// 轮询:是否有客户端连接
SocketChannel clientChannel = null;
while (clientChannel == null) {
    clientChannel = serverChannel.accept();
}

// 轮询:查看是否有数据可读
while (clientChannel.read(buffer) == -1) ;

buffer.flip(); // 切换到读数据模式
while (buffer.hasRemaining()) {
    clientChannel.write(buffer); // echo, 读了啥就写啥
}

上述代码充斥着 while 轮询,显然不够优雅,多路复用 I / O 模型便用来解决这个问题

多路复用 I / O 模型是网络编程中非常经典的一种 I / O 模型
为了实现多路复用 I / O 模型,Unix 提供了 epoll 库,Windows 提供了 iocp 库,BSD 提供了 kequeue 库 ...
Java 作为一种跨平台语言,对不同操作系统的实现方式进行了封装,提供了统一的 Selector

我们可以将需要监听的 Channel,调用 register() 函数,注册到 Selector 中,Selector 底层会通过轮询的方式,查看哪些 Channel 可读、可写、可连接等,并将其返回处理
关于 Selector 的使用示例代码,我们在接下来的「Java I / O 模型」小节中给出

1.4、异步 Channel

尽管使用 Selector 可以避免程序员自己手写轮询代码,但是 Selector 底层仍然依赖轮询来实现,在 JDK 7 中,java.nio 类库做了升级,引入了支持异步模式的 Channel
主要包括:AsynchronousFileChannel、AsynchronousSocketChannel、AsynchronousServerSocketChannel,而前面讲到的 Channel 都是同步模式的

什么是同步模式?什么是异步模式呢?同步和异步这两个概念,跟阻塞和非阻塞又是否有联系呢?我们通过一个生活中的例子来给你形象解释一下
阻塞(等)、非阻塞(不等 + 可以干别的 + 轮询)、同步(没人叫你 + 需要轮询)、异步(有人叫你)

假设你去一家餐厅就餐,因为就餐的人太多,需要取号等位

  • 取号之后,如果你站在餐厅门口一直等待被叫号,啥都不干,那么这就是 "阻塞模式"
    如果你先去商场里逛一逛,一会回来看一下有没有轮到你,没有就继续再去逛,那么这就是 "非阻塞模式"
  • 如果你在取号时,登记了手机号码,那么你就可以放心去逛商场了,等叫到你的号时,服务员会打电话通知你,这就是 "异步模式"
    如果需要自己去查看有没有轮到你,不管是阻塞模式还是非阻塞模式,都是 "同步模式"

实际上,异步模式下也可以有阻塞和非阻塞之分

  • 如果在没有收到通知时,尽管你可以去干其他事情,但你偏偏就啥都不干,就站在门口等着被叫号,那么这就是 "阻塞异步模式"
  • 如果你选择享受通知服务,去干其他事情,那么这就是 "非阻塞异步模式"

从上面的解释,我们可以发现,"同步、异步" 跟 "阻塞、非阻塞" 没有直接关系

在异步模式下,Channel 不再注册到 Selector,而是注册到操作系统内核中,由内核来通知某个 Channel 可读、可写或可连接
java.nio 收到通知之后,为了不阻塞主线程,会使用线程池去执行事先注册的回调函数
关于异步模式的用法,我们也是在「Java I / O 模型」小节中展示

2、Java IO 模型

在面试和工作中,我们经常听到 "I / O 模型" 这个概念, I / O 模型一般用于网络编程中,所以,"I / O 模型" 的全称是 "网络 I / O 模型"
除此之外, I / O 模型多数都用来指导服务器开发,相比服务器开发,客户端开发不需要处理多个并发连接的情况,往往会简单一些,也就不需要这些复杂的模型

Java 中常被提及的 I / O 模型有三个:阻塞 I / O 模型(BIO)、非阻塞 I / O 模型(NIO)、异步 I / O 模型(AIO),我们依次看下这 3 种常见的 I / O 模型

2.1、阻塞 I / O 模型(BIO)

前面讲过, I / O 访问模式有两种:阻塞模式和非阻塞模式,阻塞 I / O 模型指的是利用阻塞模式来实现服务器,一般来说,这种模型需要配合多线程来实现

一般来讲,服务器需要连接大量客户端,因为 read() 函数是阻塞函数
所以为了实时接收客户端发来的数据,服务器需要创建大量线程,每个线程负责等待读取(调用 read() 函数)一个客户端的数据
因为 java.io 支持阻塞模式,java.nio 既支持阻塞模式又支持非阻塞模式,所以,java.io 和 java.nio 都可以实现阻塞 I / O 模型

2.1.1、示例

我们使用 java.io 来编写示例代码,如下所示
使用 java.io 进行网络编程,需要配合 java.net 类库
比如在下面代码中:Socket、ServerSocket 都是 java.net 包中的类,java.net 类库用于管理连接、java.io 用于读写数据

public class BioEchoServer {

    private static class ClientHandler implements Runnable {
        private Socket socket;

        public ClientHandler(Socket socket) {
            this.socket = socket;
        }

        @Override
        public void run() {
            byte[] data = new byte[1024];
            // 持续接收客户端发来的数据
            while (true) {
                try {
                    // read() 为阻塞函数, 直到读取到数据再返回
                    socket.getInputStream().read(data);
                    // write() 为阻塞函数, 全部写完成才会返回
                    socket.getOutputStream().write(data); // echo
                } catch (IOException e) {
                    // log and exit
                    break;
                }
            }
        }
    }

    public static void main(String[] args) throws IOException {
        ServerSocket serverSocket = new ServerSocket();
        serverSocket.bind(new InetSocketAddress("127.0.0.1", 1192));

        while (true) {
            // accept() 为阻塞函数, 直到有连接到来才返回
            Socket clientSocket = serverSocket.accept();
            // 为了每个客户端单独创建一个线程处理
            new Thread(new ClientHandler(clientSocket)).start();
        }
    }
}

2.1.2、缺点

如果有 n 个客户端连接服务器,那么服务器需要创建 n + 1 个线程,其中 n 个线程用于调用 read() 函数,除此之外,因为 accept() 函数也是阻塞函数,所以也独占一个线程
当连接的客户端非常多时,服务器需要创建大量线程,而每个线程会分配一个线程栈,需要占用一定的内存空间,当线程比较多时,内存资源的消耗就会比较大
大量线程来回切换,也会导致服务器整体处理性能的下降
除此之外,大部分线程可能都阻塞在 read() 函数上,等待数据的到来,什么都不做但又要白白占用内存和线程资源,非常浪费

总结:服务器需要创建大量线程,内存资源的消耗比较大;大量线程来回切换导致处理性能下降;大部分线程可能处于阻塞状态,白白占用内存和线程资源

2.2、非阻塞 I / O 模型(NIO)

非阻塞 I / O 模型指的是利用非阻塞模式来开发服务器,一般需要配合 Selector 多路复用器,所以这种模型也叫做多路复用 I / O 模型
不过这两种叫法都有点以偏概全,所以你不必太纠结于名称,知道这种模型具体是如何实现的即可

2.2.1、示例

因为 java.io 只支持阻塞模式,所以这种模型只能通过 java.nio 来实现
非阻塞 I / O 模型的示例代码如下所示,利用 java.nio 进行网络编程,也像 java.io 那样,需要 java.net 类库的配合,比如代码中的 InetSocketAddress 就是 java.net 中的类

public class NioEchoServer {

    public static void main(String[] args) throws IOException {
        // Selector
        Selector selector = Selector.open();

        // 创建 serverChannel 并注册到 selector 监听 "是否有客户端连接到来"
        ServerSocketChannel serverChannel = ServerSocketChannel.open();
        serverChannel.bind(new InetSocketAddress("127.0.0.1", 1192));
        serverChannel.configureBlocking(false); // 设置为非阻塞模式, 直接返回
        serverChannel.register(selector, SelectionKey.OP_ACCEPT);

        ByteBuffer buffer = ByteBuffer.allocate(1024);
        while (true) {
            int channelCount = selector.select(); // 非阻塞, 直接返回
            if (channelCount > 0) {
                Set<SelectionKey> keys = selector.selectedKeys();
                Iterator<SelectionKey> iterator = keys.iterator();

                while (iterator.hasNext()) {
                    SelectionKey key = iterator.next();

                    // 有客户端连接到来: 创建 clientChannel 并注册到 selector 监听 "是否有数据可读"
                    if (key.isAcceptable()) {
                        SocketChannel clientChannel = serverChannel.accept();
                        clientChannel.configureBlocking(false); // 设置为非阻塞模式, 直接返回
                        clientChannel.register(selector, SelectionKey.OP_READ);
                    }

                    // clientChannel 有数据可读
                    else if (key.isReadable()) {
                        SocketChannel clientChannel = (SocketChannel) key.channel();
                        clientChannel.read(buffer);
                        buffer.flip(); // 切换到读数据模式
                        if (buffer.hasRemaining()) {     // 也可以注册到 selector 中
                            clientChannel.write(buffer); // echo, 读了啥就写啥
                        }
                        buffer.clear(); // 重复利用
                    }
                }
            }
        }
    }
}

在 NioEchoServer 类中,如果有 n 个客户端连接服务器,那么就会创建 n + 1 个 Channel,其中一个 serverChannel 用于接受客户端的连接,另外 n 个 clientChannel 用于与客户端进行通信
这 n + 1 个 Channel 均注册到 Selector 中,Selector 会间隔一定时间轮训这 n + 1 个 Channel,查找可连接、可读的 Channel,然后再进行连接、读取操作

如上代码所示,大部分情况下,我们都不需要监听 Channel 是否可写,毕竟网络写入跟文件写入类似,大部分情况下都不需要等待
只有当写入出现问题时,比如 write() 函数返回 0,表示网络拥塞,此时才需要如下代码所示,将 Channel 注册到 Selector 中,等待可写

clientChannel.register(selector, SelectionKey.OP_WRITE);

需要注意的是,并不是所有的 Channel 都可以注册到 Selector 中被监听,只有实现了 SelectableChannel 接口的 Channel 才可以
比如 DatagramChannel、SocketChannel、ServerSocketChannel
FileChannel 因为没有实现 SelectableChannel,并且不支持非阻塞模式,所以无法被 Selector 监听

2.2.2、缺点

多路复用 I / O 模型只需要一个线程即可,解决了阻塞 I / O 模型线程开销大的问题,不过这种模型依然存在问题
如果某些 clientChannel 读写的数据量比较大,或者逻辑操作比较复杂,耗时比较久
因为所有的工作都在一个线程中完成,那么其他 clientChannel 便迟迟得不到处理,最终的效果就是,服务器响应客户端的延迟很大

2.2.3、解决

为了解决这个问题,我们可以引入线程池,对于 Selector 检测到有数据可读的 clientChannel,我们从线程池中取线程来处理,而不是所有的 clientChannel 都在一个线程中处理
我们知道,阻塞 I / O 模型也用到了多线程,跟这里的区别在于

  • 不管有没有数据可读,阻塞 I / O 模型中的每个 clientSocket 都会一直占用线程
  • 而这里的多线程只会处理经过 Selector 筛选之后有可读数据的 clientChannel,并且处理完之后就释放回线程池,线程的利用率更高

2.3、异步 I / O 模型(AIO)

2.3.1、示例

实际上,上述问题使用 java.nio 的异步 Channel 实现起来更加优雅,如下代码所示
通过异步 Channel 调用 accept()、read()、write() 函数,当有连接建立、数据读取完成、数据写入完成时,底层会通过线程池执行对应的回调函数
这种服务器的实现方式叫做异步 I / O 模型

public class AioEchoServer {

    /**
     * 读完成处理程序
     */
    private static class ReadCompletionHandler implements CompletionHandler<Integer, ByteBuffer> {
        private AsynchronousSocketChannel clientChannel;

        public ReadCompletionHandler(AsynchronousSocketChannel clientChannel) {
            this.clientChannel = clientChannel;
        }

        // result 表示读取到的数据长度
        // attachment 是在 read() 方法中传递的对象, 用于在回调函数中执行一些操作
        @Override
        public void completed(Integer result, ByteBuffer buffer) {
            buffer.flip(); // 切换到读数据模式
            // 异步 write(), 回调函数为 null, 写入完成就不用回调了
            clientChannel.write(buffer, null, null); // echo, 读了啥就写啥
        }

        @Override
        public void failed(Throwable exc, ByteBuffer attachment) {
            // log exc exception
        }
    }

    /**
     * 接受完成处理程序
     */
    private static class AcceptCompletionHandler implements CompletionHandler<AsynchronousSocketChannel, Object> {

        private AsynchronousServerSocketChannel serverChannel;

        public AcceptCompletionHandler(AsynchronousServerSocketChannel serverChannel) {
            this.serverChannel = serverChannel;
        }

        // result 是 AsynchronousSocketChannel 对象, 表示客户端连接成功后返回的 SocketChannel 对象
        // attachment 参数是在 accept() 方法中传递的对象, 用于在回调函数中执行一些操作
        @Override
        public void completed(AsynchronousSocketChannel clientChannel, Object attachment) {
            // in order to accept other client's connections
            serverChannel.accept(attachment, this);
            ByteBuffer buffer = ByteBuffer.allocate(1024); // 读取到的数据会写入到该缓冲区中
            // 异步 read()
            clientChannel.read(buffer, buffer, new ReadCompletionHandler(clientChannel));
        }

        @Override
        public void failed(Throwable exc, Object attachment) {
            // log exc exception
        }

    }

    public static void main(String[] args) throws IOException, InterruptedException {
        AsynchronousServerSocketChannel serverChannel = AsynchronousServerSocketChannel.open();
        serverChannel.bind(new InetSocketAddress("127.0.0.1", 1192));

        // 异步 accept()
        serverChannel.accept(null, new AcceptCompletionHandler(serverChannel));
        Thread.sleep(Integer.MAX_VALUE);
    }
}

实际上,在平时的开发中,我们一般不会直接使用底层的 java.nio 类库
而是使用 Netty 等框架来进行网络编程,这些框架封装了网络编程的复杂性,使用起来更加简单,开发效率更高
除了以上三种常见的 I / O 模型之外,实际上,还有更多更加复杂的 I / O 模型
比如 Netty 框架提供的 Reactor 模型,关于 Netty 等网络编程知识,我们就不深入讲解了,毕竟专栏的重点不在这里

2.3.2、解释

这段程序是一个基于 Java NIO2(即 AIO)实现的简单的 Echo 服务器,使用了异步 IO 模型
程序中主要使用了两个类:AsynchronousServerSocketChannel 和 AsynchronousSocketChannel

  • AsynchronousServerSocketChannel 是一个异步的服务端 Socket 通道,用于监听客户端的连接请求
    当客户端连接成功后,会通过回调函数的方式通知程序,并返回一个 AsynchronousSocketChannel 对象,用于与客户端进行通信
  • AsynchronousSocketChannel 是一个异步的客户端 Socket 通道,用于与服务端进行通信
    可以进行异步的读写操作,当读写操作完成后,会通过回调函数的方式通知程序

程序中定义了两个 CompletionHandler 类,分别为 ReadCompletionHandler 和 AcceptCompletionHandler

  • ReadCompletionHandler 类实现了 CompletionHandler 接口,用于处理异步读取数据的完成事件
    当读取数据完成后,回调函数会被调用,然后将读取到的数据写回客户端
  • AcceptCompletionHandler 类实现了 CompletionHandler 接口,用于处理异步接受客户端连接的完成事件
    当客户端连接成功后,回调函数会被调用,然后将客户端的 SocketChannel 注册到 Selector 中,并开始异步读取客户端发送的数据

在 main 函数中,首先创建了一个 AsynchronousServerSocketChannel 对象,然后通过 bind() 方法绑定到指定的地址和端口上
接着调用 accept() 方法开始监听客户端连接请求,并传入一个 AcceptCompletionHandler 对象作为回调函数,用于处理连接请求的完成事件
程序会一直阻塞在 Thread.sleep() 方法处,等待客户端连接

2.3.3、说明

你可能还听过其他 I / O 模型的分类,比如在《Unix 网络编程》一书中,介绍了 Unix 操作系统的 5 种 I / O 模型
阻塞 I / O 模型、非阻塞 I / O 模型、多路复用 I / O 模型、信号驱动 I / O 模型、异步 I / O 模型
那么,Unix 操作系统下的 I / O 模型跟 Java I / O 模型有什么联系呢?

实际上,不同的操作系统会提供不同的 I / O 模型,Java 是一种跨平台语言,为了屏蔽各个操作系统 I / O 模型的差异
设计了 3 种新的 I / O 模型:BIO(阻塞 I / O)、NIO(非阻塞 I / O)、AIO(异步 I / O),并且提供了 I / O 类库来支持这 3 种 I / O 模型的代码实现
而 Java 的 I / O 类库底层需要依赖操作系统的 I / O 接口(专业名称为系统调用)来实现,因此从本质上来讲,Java I / O 模型只是对操作系统 I / O 模型的重新封装

3、对比 java.io 与 java.nio

在功能上来看,java.nio 完全可以替代 java.io,那么在平时开发中,我们是不是应该首选 java.nio 呢?在新开发的项目中,是不是就不应该使用老的 java.io 呢?

实际上,在某些情况下,我们确实必须使用 java.nio,比如网络编程
尽管使用 java.io,并配合 java.net,也可以进行网络编程,但 java.io 只支持阻塞模式,只能实现阻塞 I / O 模型,对于大部分网络编程来说,都是不够的
而 java.nio 提供了非阻塞模式、Selector 多路复用器、异步模式,能够实现更加高性能的网络模型,比如非阻塞 I / O 模型、异步 I / O 模型
相比 java.io 而言,在网络编程方面,java.nio 的优势更加明显

但是,在某些情况下,到底使用 java.io 还是 java.nio,会有一些争论,比如文件读写
前面提到,文件读写只支持阻塞模式,因此,使用 java.io 或 java.nio 都可以
有些人认为,使用 java.io 进行文件读写,代码编写更加简单
有些人则认为,java.nio 的文件读写功能更加丰富
我个人认为,既然有争论,就说明两者没有哪个更有绝对优势,不然也就不会有争论了
因此,对于使用 java.io 还是 java.nio 进行文件读写,按照你的喜好或者团队的编程习惯来选择就好

总结一下的话,对于网络编程,我们首选 java.nio,对于文件读写,java.io 和 java.nio 都可以

4、课后思考题

java.io 提供了 BufferedInputStream、BufferedOutputStream,用于支持缓存的文件读写,那么类似功能,java.nio 是如何实现的呢?

BufferedInputStream、BufferedOutputStream 是包装器类,在 InputStream、OutputStream 之上提供缓存功能
在 java.nio 中,缓存采用 Buffer 来实现,比如 ByteBuffer、CharBuffer 等
posted @ 2023-06-08 16:02  lidongdongdong~  阅读(71)  评论(0编辑  收藏  举报