从零开始学Java之NIO模型又是怎么回事?

2023年 10月 16日 22.1k 0

作者:孙玉昌,昵称【一一哥】,另外【壹壹哥】也是我哦

千锋教育高级教研员、CSDN博客专家、万粉博主、阿里云专家博主、掘金优质作者

前言

在上一篇文章中,壹哥给大家介绍了BIO模型,我们知道在BIO模型中存在着诸多的弊端,比如线程开销大、同步阻塞IO导致IO效率低下、可靠性较差、无法实现真正的高并发等。总之,由于BIO模型在高并发访问情况下存在的性能瓶颈,无法实现真正的高并发,无法满足现代互联网高并发的需求。所以在新的需求中,我们需要对BIO模型进行改进,这就是NIO模型的由来。

------------------------------前戏已做完,精彩即开始----------------------------

全文大约【3200】 字,不说废话,只讲可以让你学到技术、明白原理的纯干货!本文带有丰富的案例及配图视频,让你更好地理解和运用文中的技术概念,并可以给你带来具有足够启迪的思考......

配套开源项目资料

Github: github.com/SunLtd/Lear…

Gitee: gitee.com/sunyiyi/Lea…

一. NIO-非阻塞式IO

1. NIO简介

我们知道,在BIO模式下,如果一个线程在执行IO操作时发生了阻塞,它就会一直等待,直到数据被读取或者发生了超时等错误才会终止阻塞。所以当有大量客户端连接时,服务器端就需要创建大量的线程来处理这些连接,但大量线程的创建和切换会带来极大的资源消耗和性能问题。

为了解决这个问题,JDK 1.4中引入了NIO(Non-Blocking I/O)模式,有的地方也称其为New IO,即New Input/Output。在NIO模式下,一个线程可以同时处理多个客户端连接的I/O操作,而不需要为每个客户端的连接都创建一个线程。这样就大大降低了线程创建和切换的开销,从而提高了服务器的并发性能。

NIO模式提供了与标准BIO API不同的工作方式,数据的读取和写入都是非阻塞的。当数据还没有准备好被读取时,读操作就会立即返回,这样线程就不会阻塞。而当数据准备好后,会通过Selector通知应用程序进行读取。写操作也类似,当数据无法立即被写入时,写操作会立即返回,这样线程也不会阻塞。

2. 基本概念

NIO模式下有三个核心组件,包括Channel(通道)、Buffer(缓冲区)和Selector(选择器) 。其中,Channel表示一个打开的连接,可以进行读写操作;Buffer是一个容器,用于存储数据;Selector可以监听多个Channel,当某个Channel上的数据可读或可写时,就会通知应用程序来进行处理。接下来壹哥再把这三个核心组件再分别介绍一下。

2.1 Channel通道

Channel表示一个可以进行IO操作的开放连接,如一个文件或一个网络套接字,类似于是IO流Stream。Channel通道给我们提供了一种新的IO操作方式,可以在缓冲区和IO设备之间快速高效地进行数据传输。目前Channel通道有两种类型:

  • 文件通道:用于文件的读写操作,支持读写、截取、映射和锁定操作;
  • 套接字通道:用于网络通信的读写操作,支持非阻塞模式和多路复用模式。

Channel通道具有如下特点:

  • 可以同时进行读写操作;
  • 可以进行异步地读写;
  • 可以从Channel中读取数据到Buffer,也可以将Buffer中的数据写入Channel中。

目前常用的Channel API包括:

  • FileChannel:用于文件的读写操作;
  • DatagramChannel:基于UDP协议,进行数据的读写操作;
  • SocketChannel:基于TCP协议,进行客户端的读写操作;
  • ServerSocketChannel:基于TCP协议,进行服务器端的读写操作。

2.2 Buffer缓冲区

Buffer是一个容器对象,用于存储数据。在NIO中,所有的数据都是用Buffer缓冲区来处理的。缓冲区实质上是一个数组,可以保存多个数据类型,其内部有一个指针,可以指向下一个待读取或写入的位置。Buffer缓冲区有两种类型:

  • 直接缓冲区:该缓冲区是在操作系统的内存中分配的,可以避免在Java堆和操作系统间的复制。
  • 非直接缓冲区:该缓冲区是在Java堆中分配的,可以在与操作系统间进行数据交换时进行复制。

Buffer具有如下特点:

  • 可以读写数据;
  • 有一个position指针,代表下一个要读写的位置;
  • 有一个limit指针,代表数据的长度,读写操作不能超出这个范围;
  • 有一个capacity指针,代表Buffer的总大小。

因为Buffer是一个顶层接口,目前有以下几个具体的实现类:

  • ByteBuffer:用于字节数据的存取操作;
  • CharBuffer:用于字符数据的存取操作;
  • ShortBuffer、IntBuffer、LongBuffer、FloatBuffer、DoubleBuffer:用于对应数值型数据的存取操作。

Buffer的基本使用流程如下:

  • 创建Buffer对象;
  • 写入数据到Buffer中;
  • 调用flip()方法,将Buffer从写模式切换到读模式;
  • 从Buffer中读取数据;
  • 调用clear()或compact()方法,将Buffer从读模式切换到写模式。

2.3 Selector选择器

Selector选择器是NIO用于检查一个或多个Channel状态是否处于可读、可写等事件的核心组件。简单来说,Selector可以管理所有打开的通道,并监听多个通道上的事件,在有事件发生时立即处理。

尤其是Selector可以实现在单线程中管理多个Channel。当一个Channel中有数据可读或可写时,Selector会自动将其加入到待处理队列中,等待Selector处理,从而可以实现多路复用,提高了程序的性能,避免了不必要的CPU消耗。

3. NIO开发步骤

如果我们想进行NIO开发,可以遵循以下几个步骤:

  • 创建一个Selector对象;
  • 创建一个或多个Channel对象,并将其注册到Selector中;
  • 创建一个Buffer对象,用于存储读取到的数据;
  • 调用Selector的select()方法,等待数据的到来;
  • 遍历Selector的selectedKeys集合,处理已经就绪的Channel;
  • 从Channel中读取数据到Buffer中;
  • 调用Buffer的flip()方法进行模式切换。
  • 接下来壹哥就通过两个案例来给大家演示如何在NIO中进行数据的读写,请大家继续往下看吧。

    4. 读取数据

    4.1 读取流程

    NIO读取数据的流程大致如下:

  • 创建一个Buffer对象,用于保存读取到的数据;
  • 创建一个Channel对象,用于读取数据;
  • 将数据从Channel读取到Buffer中;
  • 从Buffer中读取数据并进行处理。
  • 4.2 代码案例

    下面就是壹哥设计的NIO读取数据的案例,如下所示:

    import java.io.FileInputStream;
    import java.nio.ByteBuffer;
    import java.nio.channels.FileChannel;
    
    /**
     * @author 一一哥Sun
     */
    public class Demo16 {
        public static void main(String[] args) {
            try {
                FileInputStream fileInputStream = new FileInputStream("F:/a.txt");
                // 1.创建Channel对象
                FileChannel fileChannel = fileInputStream.getChannel();
                // 2.设置缓冲区
                ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
                // 3.将数据从Channel读取到Buffer中
                while (fileChannel.read(byteBuffer) != -1) {
                    //4.切换到读模式
                    byteBuffer.flip();
                    // 判断是否还有内容
                    while (byteBuffer.hasRemaining()) {
                        //5.读取数据并进行处理
                        System.out.print((char) byteBuffer.get());
                    }
                    //6.切换到写模式,为下次读取数据做准备
                    byteBuffer.clear();
                }
                // 关闭资源
                fileInputStream.close();
             } catch (Exception e) {
                 e.printStackTrace();
             }
        }
    }
    

    5. 写入数据

    我们可以利用NIO实现数据的读取,当然也可以实现数据的写入。NIO提供了多种写入数据的方式,其中最常见的是通过Channel和Buffer进行写入。接下来壹哥就给大家介绍一下如何使用Channel和Buffer来写入数据。

    5.1 写入流程

    NIO写入数据的流程大致如下:

  • 创建一个Buffer缓冲区对象,用于保存要写入的数据;
  • 创建一个Channel对象,用于写入数据;
  • 将数据从Channel写入到Buffer中;
  • 关闭释放资源。
  • 接下来壹哥就讲解一下写入数据的代码实现。

    5.2 代码案例

    利用NIO写入数据的代码案例如下所示:

    import java.io.IOException;
    import java.nio.ByteBuffer;
    import java.nio.channels.FileChannel;
    import java.nio.file.Path;
    import java.nio.file.Paths;
    import java.nio.file.StandardOpenOption;
    
    /**
     * @author 一一哥Sun
     */
    public class Demo17 {
        public static void main(String[] args) {
            //1.创建一个Channel对象
            Path path = Paths.get("F:/d.txt");
            try (FileChannel channel = FileChannel.open(path, StandardOpenOption.WRITE, StandardOpenOption.CREATE)) {
                //2.创建Buffer缓冲区,并存入数据
                String data = "hello, 跟一一哥学Java吧!";
                ByteBuffer buffer = ByteBuffer.wrap(data.getBytes());
                //3.将缓冲区里的数据写入到文件中
                channel.write(buffer);
                //4.关闭资源
                channel.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
    

    在上面的案例中,首先我们获取了一个WritableByteChannel的子类对象FileChannel,该对象就可以写入数据到一个文件中。我们在try代码块中获取到了FileChannel实例,并指定文件的打开选项,其中WRITE表示可写入数据,CREATE表示在文件不存在的情况下创建新文件。

    接下来我们创建了一个ByteBuffer来存储要写入的数据。在这个例子中,壹哥使用了wrap方法将一个字节数组包装成一个ByteBuffer对象。

    然后,我们又将ByteBuffer中的数据写入到了Channel中。write方法会将ByteBuffer中的数据写入到Channel中,返回值表示实际写入的字节数。在这个例子中我们写入的是一个字符串,所以返回值应该是字符串的字节数。

    最后,我们把Channel资源关闭释放。

    6. 注意事项

    我们在使用Channel和Buffer读取和写入数据时,需要注意以下几点:

  • 调用write方法之前,需要确保ByteBuffer中包含了要写入的数据;
  • 写入数据之前,需要调用clear方法清空ByteBuffer,以确保它的位置和限制都设置正确;
  • Channel的实现类通常会使用缓存,所以在写入数据后需要调用force方法,将缓存中的数据刷新到磁盘中;
  • 在完成所有写入操作后需要关闭Channel。
  • 除了使用Channel和Buffer写入数据,NIO还提供了其他写入数据的方式,比如使用FileChannel的transferTo和transferFrom方法,以及使用SocketChannel和ServerSocketChannel等网络编程相关的类。这些内容壹哥会开辟NIO专栏进行讲解,欢迎大家持续关注壹哥哦。

    ------------------------------正片已结束,来根事后烟----------------------------

    二. 结语

    NIO模型相比BIO有了很大的改进提升,其重点内容如下:

    • NIO模型是非阻塞式的IO模型,数据的读取和写入都是非阻塞的;
    • NIO模型中,一个线程可以同时处理多个客户端连接的I/O操作,不需要为每个客户端的连接都创建一个线程;
    • NIO模型大大降低了线程的创建和切换开销,从而提高了服务器的并发性能;
    • NIO模式有三个核心组件,包括Channel(通道)、Buffer(缓冲区)和Selector(选择器)。

    在下一篇文章中,壹哥会给大家介绍AIO异步非阻塞式模型,敬请大家继续关注。另外如果你独自学习觉得有很多困难,可以加入壹哥的学习互助群,大家一起交流学习。

    相关文章

    JavaScript2024新功能:Object.groupBy、正则表达式v标志
    PHP trim 函数对多字节字符的使用和限制
    新函数 json_validate() 、randomizer 类扩展…20 个PHP 8.3 新特性全面解析
    使用HTMX为WordPress增效:如何在不使用复杂框架的情况下增强平台功能
    为React 19做准备:WordPress 6.6用户指南
    如何删除WordPress中的所有评论

    发布评论