一、到底什么是BIO、NIO、AIO?

这些可以理解为是Java语言对操作系统的各种IO模型的封装,程序员在使用这些API的时候,不需要关系操作系统层面的知识,也不需要根据不同操作系统编写不同的代码,只需要使用Java的API就可以了。

二、BIO、NIO、AIO的区别

1.BIO就是传统的java.io包(意思就是你在使用java.io包进行输入输出操作的时候,就是使用的BIO通信机制),BIO是传统的同步阻塞式的I/O。也就是说在读入输入流或者输出流时,在读写操作完成之前,线程会一直阻塞,会一直占用CPU资源,直到读写操作完成之后,才继续完成下面的任务。因此每个请求都需要有一个线程来单独处理。

采用BIO通信模型的服务端,通常有一个独立的Acceptor线程负责监听客户端的连接,它接收到客户端的连接请求之后,会为客户端每一个连接请求创建一个新的线程进行处理,处理完之后,通过输出流返回给客户端,线程销毁,这就是典型的一请求一应答模型。

 可以看出,这是在多线程情况下执行的。当在单线程环境条件下,在while循环中服务端会调用accept方法等待接收客户端的连接请求,一旦收到这个连接请求,就可以建立socket,并在socket上进行读写操作,此时不能再接收其他客户端的连接请求,只能等待同当前服务端连接的客户端的操作完成或者连接断开。

该模型最大的缺单就是缺乏弹性伸缩能力,当客户端并发访问量增加后,服务端的线程个数和客户端并发访问数呈1:1的正比关系,由于线程是java虚拟机非常宝贵的系统资源,当线程数膨胀之后,系统的性能会急剧下降,随着并发量的继续增大,系统会发生线程堆栈溢出、创建新线程失败等问题,并最终导致进程宕机或者僵死,不能对外提供服务。

BIO实现的代码:

public class server {
    private static Socket socket=null;
    public static void main(String[] args) {
        try {
            //绑定端口
            ServerSocket serverSocket=new ServerSocket();
            serverSocket.bind(new InetSocketAddress(8080));
            while (true){
                //等待连接  阻塞
                System.out.println("等待连接");
                socket = serverSocket.accept();
                System.out.println("连接成功");
                //连接成功后新开一个线程去处理这个连接
                new Thread(new Runnable() {
                    @Override
                    public void run() {
                        byte[] bytes=new byte[1024];
                        try {
                            System.out.println("等待读取数据");
                            //等待读取数据    阻塞
                            int length=socket.getInputStream().read(bytes);
                            System.out.println(new String(bytes,0,length));
                            System.out.println("数据读取成功");
                        } catch (IOException e) {
                            e.printStackTrace();
                        }
                    }
                }).start();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

2.NIO是Java 1.4引入的java.nio包,提供了Channel、Selector、Buffer等新的抽象,可以构建多路复用的、同步非阻塞式IO程序,同时提供了更接近操作系统底层高性能的数据操作方式。NIO通过使用单线程轮询多个连接的的方式来实现高效的处理方式,可以支持较大数量的并发连接,但编程模型较为复杂。

NIO是为了解决BIO的缺陷提出的通信模型,以socket.read()为例:

传统的BIO里面的socket.read(),如果TCP RecvBuffer里没有数据,函数会一直阻塞,直到收到数据。

对于NIO,如果TCP RecvBuffer有数据,就把数据从网卡读到内存,并且返回给用户;反之则直接返回0,永远不会阻塞。

BIO关心的是“我要读”的问题,NIO关心的是“我可以读了”的问题。

NIO一个重要的特点是:socket主要的读、写、注册和接收函数,在等待就绪阶段都是非阻塞的,真正的I/O操作是同步阻塞的(消耗CPU但性能非常高)。

2.1 什么是Channel?

Channel,翻译过来就是“通道”,就是数据传输的管道,类似于“流”,但是与“流”又有着区别。

Channel与“流”的区别:

  • 既可以从Channel中读取数据,又可以写数据到Channel,但流的读写通常是单向的——输入输出流
  • 通道可以异步读写
  • 通道中的数据总是先读取到buffer(缓冲区),或者总是需要从一个buffer写入,不能直接访问数据
为什么Channel支持异步读写?

Java的Channel可以实现异步读写的原因主要有以下几点:

  1. 非阻塞特性:Channel在设计上采用了非阻塞的特性,它不会像传统的流一样在读写操作上阻塞线程,而是立即返回结果,告诉调用者当前的状态。这使得程序可以在等待数据准备的过程中同时进行其他操作,实现了非阻塞IO。

  2. 事件通知机制:Channel通常搭配选择器(Selector)来使用,选择器能够检测多个Channel的就绪状态,如是否可读、可写等,并通过事件通知(例如轮询或回调)及时地通知程序哪些Channel处于就绪状态,从而可以进行相应的读写操作。这种机制支持程序实现异步IO模型。

  3. 操作系统底层支持:Channel的异步读写也依赖于操作系统底层的异步IO支持。Java NIO中的Channel实际上是对操作系统底层异步IO的封装和抽象,利用了操作系统提供的异步IO机制来实现其自身的异步读写功能。

2.2 什么是Buffer?

Buffer是一个对象,里面是要写入或者读出的数据,在java.nio库中,所有的数据都是用缓冲区处理的。

在读取数据时,它是直接读到缓冲区中的;在写入数据时,也是直接写到缓冲区中,任何时候访问Channel中的数据,都是通过缓冲区进行操作的。

缓冲区实质上是一个数组,通常是一个字节数组ByteBuffer,当然也有其他类型的:

2.3 什么是Selector? 

Selector被称为选择器,Selector会不断地轮询注册在其上的Channel,如果某个Channel上发生读或写事件,这个Channel就被判定处于就绪状态,会被Selector轮询出来,然后通过SelectionKey可以获取到就绪Channel的集合,进行后续的I/O操作。

一个多路复用器Selector可以同时轮询多个Channel,JDK使用了epoll()代替了传统的select实现,所以并没有最大连接句柄的限制,这意味着只需要一个线程负责Selector的轮询,就可以接入成千上万的客户端。

一些名词解释

select实现

select方法是一种用于实现IO多路复用的系统调用,在Unix/Linux操作系统中广泛使用。其主要作用是监视多个文件描述符,当其中任何一个文件描述符就绪(可读、可写或异常)时,通知程序进行相应的IO操作。以下是select方法的基本介绍:

  1. 功能:select方法允许程序同时监视多个文件描述符的IO状态,包括可读、可写和异常状态,从而实现IO多路复用,提高程序的效率。

  2. 使用:程序需要将需要监视的文件描述符集合传递给select方法,并设置超时时间(可以设为NULL表示一直等待)。select方法会阻塞程序,直到其中一个文件描述符就绪或超时为止。

  3. 返回值:select方法返回就绪的文件描述符数量,如果超时则返回0,如果出错则返回-1。程序可以通过检查文件描述符集合的状态来确定哪些文件描述符处于就绪状态。

  4. 限制:select方法在监视文件描述符时有最大连接句柄数限制,一般不能超过1024个文件描述符。这限制了select方法在处理大规模并发连接时的效率。

  5. 缺点:select方法由于使用位图来表示文件描述符集合,需要线性遍历所有文件描述符,导致效率降低,特别是在大量文件描述符情况下。

总的来说,select方法是一种经典的IO多路复用机制,虽然存在一些限制和缺点,但在某些场景下仍然具有一定的应用价值。对于大规模并发连接的情况,通常会选择更高效的方法,比如epoll。

位图

位图(Bitmap)是一种数据结构,用于表示若干个比特位的集合。在计算机中,每一位(bit)可以只有两个取值,通常是0或1,因此位图实际上是一个由0和1组成的序列,用来表示某些状态或信息。

举例说明,如果我们有8个开关,每个开关只有两种状态:打开或关闭。我们可以使用一个8位的位图来表示这些开关的状态情况。比如,假设我们想表示这8个开关的状态如下:

  • 01101001

其中每一位代表一个开关,0表示关闭,1表示打开。通过这个位图,我们可以清晰地知道每个开关当前的状态,而且用8个bit就能表示8个开关的状态,非常紧凑高效。

在计算机科学中,位图常常被用于处理大规模的数据或进行高效的状态存储。它可以用于表示集合、存储标记、进行快速搜索和过滤等操作。在文件系统、数据库索引、图形处理等领域,位图都有着广泛的应用。

文件描述符

2.4 Buffer、Selector、Channel之间的关系?

 2.5 NIO多路复用的实现

NIO是利用了单线程轮询事件的机制,通过高效地定位就绪的Channel,来决定做什么,仅仅select阶段是阻塞的,可以有效避免大量客户端连接时,频繁切换线程带来的问题,应用的拓展能力有了很大的提高。

  • 首先,通过Selector.open()创建一个Selector,作为类似调度员的角色;
  • 然后,创建一个ServerSocketChannel,并在Selector中注册这个Channel,通过指定的SelectionKey.OP_ACCEPT,告诉调度员,它关注的是新的连接请求;
  • 为什么我们要明确配置非阻塞模式呢?这是因为阻塞模式下,注册模式是不允许的,会抛出IIIegalBlockingModeException异常;
  • Selector阻塞在select操作,当有Channel发生接入请求,就会被唤醒;
public class server {
    public static void main(String[] args) {
        try {
            //创建一个socket通道,并且设置为非阻塞的方式
            ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
            serverSocketChannel.configureBlocking(false);//设置为非阻塞的方式
            serverSocketChannel.socket().bind(new InetSocketAddress(9000));
            //创建一个selector选择器,把channel注册到selector选择器上
            Selector selector = Selector.open();
            serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
            while(true)
            {
                System.out.println("等待事件发生");
                selector.select();
                System.out.println("有事件发生了");
                Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
                while(iterator.hasNext())
                {
                    SelectionKey key = iterator.next();
                    iterator.remove();
                    handle(key);
                }

            }
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    private static void handle(SelectionKey key) throws IOException{
        if(key.isAcceptable())
        {
            System.out.println("连接事件发生");
            ServerSocketChannel serverSocketChannel = (ServerSocketChannel) key.channel();
            //创建客户端一侧的channel,并注册到selector上
            SocketChannel socketChannel = serverSocketChannel.accept();
            socketChannel.configureBlocking(false);
            socketChannel.register(key.selector(),SelectionKey.OP_READ);
        } else if (key.isReadable()) {
            System.out.println("数据可读事件发生");
            SocketChannel socketChannel = (SocketChannel) key.channel();
            ByteBuffer buffer = ByteBuffer.allocate(1024);
            int len = socketChannel.read(buffer);
            if(len!=-1)
            {
                System.out.println("读取到客户端发送的数据:"+new String(buffer.array(),0,len));
            }
            //给客户端发送信息
            ByteBuffer wrap = ByteBuffer.wrap("hello world".getBytes());
            socketChannel.write(wrap);
            key.interestOps(SelectionKey.OP_READ|SelectionKey.OP_WRITE);
            socketChannel.close();
        }
    }

下面是多路复用的流程图:

 3.AIO是Java 1.7之后引入的包,是NIO的升级版本。异步非阻塞I/O,通过回调的方式实现高效的I/O操作,也就是应用操作之后会直接返回,不会堵塞(此时用户进程只需要对数据处理,而不需要进行实际的IO读写操作,因为真正的IO操作已经由操作系统内核完成了),当后台去处理完成之后,操作系统会通知相应的线程进行后续的操作。这样可以大大降低系统资源的消耗,适用于高并发、高吞吐量的场景,但在实际使用中可能会受到操作系统和硬件的限制。

4.可以用一个例子来描述这三个通信模型的区别:设想你要烧水这样一个场景

BIO:在烧水期间,你一直守在旁边,不敢任何事,等到水烧开了才去完成其他事

NIO:在烧水期间,你不必一直守在旁边,而是时不时来看水是否烧开,其他时间可以去完成其他事

AIO:在烧水期间,你无需来看水是否烧开了,可以去完成其他任务,水烧开的时候会发出提示音提示你水开了,这时候你再来处理。

三、一些基本概念的补充

同步和异步(线程之间的调用):

同步操作时,调用者需要等待被调用者返回结果,才会进行下一步操作

而异步则相反,调用者不需要等待被调用者返回调用,即可进行下一步操作,被调用者通常依靠事件、回调等机制来通知调用者返回调用结果

阻塞和非阻塞(线程内调用):

阻塞和非阻塞是对同一个线程来说的,在某个时刻,线程要么处于阻塞,要么处于非阻塞

阻塞和非阻塞关注的是程序在等待调用结果(消息、返回值)时的状态:

阻塞调用是指在调用结果返回之前,当前线程会被挂起,调用线程只有在得到结果之后才会返回

非阻塞调用指在不能立刻得到结果之前,该调用不会阻塞当前线程

IO分为两个操作:第一步是发起IO请求,第二步真正执行IO读写操作

同步和异步IO的概念:

同步是用户线程发起IO请求之后一直等待或者轮询内核IO操作完成之后才能继续执行后续代码。

异步是用户线程发起IO请求后仍然继续执行后续代码,当内核IO操作完成后会通知用户线程,或者调用用户线程注册的回调函数

阻塞和非阻塞IO的概念:

阻塞是指IO读写操作需要彻底完成后才能返回用户空间

非阻塞是指IO读写操作被调用后立即返回一个状态值,无需等待IO操作彻底完成

四种组合方式:

参考资料:

Java NIO三大角色Channel、Buffer、Selector相关解析 - 掘金 (juejin.cn)icon-default.png?t=N7T8https://juejin.cn/post/7037028848487104519#heading-28Java NIO 中的 Channel 详解 - 掘金 (juejin.cn)icon-default.png?t=N7T8https://juejin.cn/post/7058948260529963039JAVA中BIO、NIO、AIO的分析理解-阿里云开发者社区 (aliyun.com)icon-default.png?t=N7T8https://developer.aliyun.com/article/726698#slide-26Java核心(五)深入理解BIO、NIO、AIO - 腾讯云开发者社区-腾讯云 (tencent.com)icon-default.png?t=N7T8https://cloud.tencent.com/developer/article/1376675Java NIO浅析 - 知乎 (zhihu.com)icon-default.png?t=N7T8https://zhuanlan.zhihu.com/p/23488863彻底理解同步 异步 阻塞 非阻塞 - LittleDonkey - 博客园 (cnblogs.com)icon-default.png?t=N7T8https://www.cnblogs.com/loveer/p/11479249.html

Logo

旨在为数千万中国开发者提供一个无缝且高效的云端环境,以支持学习、使用和贡献开源项目。

更多推荐