BIO、NIO、AIO理解

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

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

二、BIO、NIO、AIO的区别

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

BIO、NIO、AIO理解_第1张图片

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

BIO、NIO、AIO理解_第2张图片

 可以看出,这是在多线程情况下执行的。当在单线程环境条件下,在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写入,不能直接访问数据

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的轮询,就可以接入成千上万的客户端。

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

BIO、NIO、AIO理解_第3张图片

 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 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();
        }
    }

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

BIO、NIO、AIO理解_第4张图片

 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操作彻底完成

四种组合方式:

BIO、NIO、AIO理解_第5张图片

参考资料:

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

你可能感兴趣的:(nio,经验分享,面试)