BIO,NIO,AIO 总结

熟练掌握 BIO,NIO,AIO 的基本概念以及一些常见问题是你准备面试的过程当中不可或缺的一部分,另外这些知识点也是你学习 Netty 的基础。java

目录:程序员

  • 1. BIO (Blocking I/O)面试

    • 1.1 传统 BIO编程

    • 1.2 伪异步 IO后端

    • 1.3 代码示例数组

    • 1.4 总结网络

  • 2. NIO (New I/O)多线程

    • 2.1 NIO 简介并发

    • 2.2 NIO的特性/NIO与IO区别app

      • 1)Non-blocking IO(非阻塞IO)

      • 2)Buffer(缓冲区)

      • 3)Channel (通道)

      • 4)Selectors(选择器)

    • 2.3 NIO 读数据和写数据方式

    • 2.4 NIO核心组件简单介绍

    • 2.5 代码示例

  • 3. AIO (Asynchronous I/O)

  • 参考

BIO,NIO,AIO 总结

Java 中的 BIO、NIO和 AIO 理解为是 Java 语言对操做系统的各类 IO 模型的封装。程序员在使用这些 API 的时候,不须要关心操做系统层面的知识,也不须要根据不一样操做系统编写不一样的代码。只须要使用Java的API就能够了。

在讲 BIO,NIO,AIO 以前先来回顾一下这样几个概念:同步与异步,阻塞与非阻塞。

同步与异步

  • 同步: 同步就是发起一个调用后,被调用者未处理完请求以前,调用不返回。

  • 异步: 异步就是发起一个调用后,马上获得被调用者的回应表示已接收到请求,可是被调用者并无返回结果,此时咱们能够处理其余的请求,被调用者一般依靠事件,回调等机制来通知调用者其返回结果。

同步和异步的区别最大在于异步的话调用者不须要等待处理结果,被调用者会经过回调等机制来通知调用者其返回结果。

阻塞和非阻塞

  • 阻塞: 阻塞就是发起一个请求,调用者一直等待请求结果返回,也就是当前线程会被挂起,没法从事其余任务,只有当条件就绪才能继续。

  • 非阻塞: 非阻塞就是发起一个请求,调用者不用一直等着结果返回,能够先去干其余事情。

那么同步阻塞、同步非阻塞和异步非阻塞又表明什么意思呢?

举个生活中简单的例子,你妈妈让你烧水,小时候你比较笨啊,在哪里傻等着水开(同步阻塞)。等你稍微再长大一点,你知道每次烧水的空隙能够去干点其余事,而后只须要时不时来看看水开了没有(同步非阻塞)。后来,大家家用上了水开了会发出声音的壶,这样你就只须要听到响声后就知道水开了,在这期间你能够随便干本身的事情,你须要去倒水了(异步非阻塞)。

1. BIO (Blocking I/O)

同步阻塞I/O模式,数据的读取写入必须阻塞在一个线程内等待其完成。

1.1 传统 BIO

BIO通讯(一请求一应答)模型图以下(图源网络,原出处不明):

图片

采用 BIO 通讯模型 的服务端,一般由一个独立的 Acceptor 线程负责监听客户端的链接。咱们通常经过在 while(true) 循环中服务端会调用 accept() 方法等待接收客户端的链接的方式监听请求,请求一旦接收到一个链接请求,就能够创建通讯套接字在这个通讯套接字上进行读写操做,此时不能再接收其余客户端链接请求,只能等待同当前链接的客户端的操做执行完成, 不过能够经过多线程来支持多个客户端的链接,如上图所示。

若是要让 BIO 通讯模型 可以同时处理多个客户端请求,就必须使用多线程(主要缘由是 socket.accept()socket.read()socket.write() 涉及的三个主要函数都是同步阻塞的),也就是说它在接收到客户端链接请求以后为每一个客户端建立一个新的线程进行链路处理,处理完成以后,经过输出流返回应答给客户端,线程销毁。这就是典型的 一请求一应答通讯模型 。咱们能够设想一下若是这个链接不作任何事情的话就会形成没必要要的线程开销,不过能够经过 线程池机制 改善,线程池还可让线程的建立和回收成本相对较低。使用FixedThreadPool 能够有效的控制了线程的最大数量,保证了系统有限的资源的控制,实现了N(客户端请求数量):M(处理客户端请求的线程数量)的伪异步I/O模型(N 能够远远大于 M),下面一节"伪异步 BIO"中会详细介绍到。

咱们再设想一下当客户端并发访问量增长后这种模型会出现什么问题?

在 Java 虚拟机中,线程是宝贵的资源,线程的建立和销毁成本很高,除此以外,线程的切换成本也是很高的。尤为在 Linux 这样的操做系统中,线程本质上就是一个进程,建立和销毁线程都是重量级的系统函数。若是并发访问量增长会致使线程数急剧膨胀可能会致使线程堆栈溢出、建立新线程失败等问题,最终致使进程宕机或者僵死,不能对外提供服务。

1.2 伪异步 IO

为了解决同步阻塞I/O面临的一个链路须要一个线程处理的问题,后来有人对它的线程模型进行了优化一一一后端经过一个线程池来处理多个客户端的请求接入,造成客户端个数M:线程池最大线程数N的比例关系,其中M能够远远大于N.经过线程池能够灵活地调配线程资源,设置线程的最大值,防止因为海量并发接入致使线程耗尽。

伪异步IO模型图(图源网络,原出处不明):

图片

采用线程池和任务队列能够实现一种叫作伪异步的 I/O 通讯框架,它的模型图如上图所示。当有新的客户端接入时,将客户端的 Socket 封装成一个Task(该任务实现java.lang.Runnable接口)投递到后端的线程池中进行处理,JDK 的线程池维护一个消息队列和 N 个活跃线程,对消息队列中的任务进行处理。因为线程池能够设置消息队列的大小和最大线程数,所以,它的资源占用是可控的,不管多少个客户端并发访问,都不会致使资源的耗尽和宕机。

伪异步I/O通讯框架采用了线程池实现,所以避免了为每一个请求都建立一个独立线程形成的线程资源耗尽问题。不过由于它的底层任然是同步阻塞的BIO模型,所以没法从根本上解决问题。

1.3 代码示例

下面代码中演示了BIO通讯(一请求一应答)模型。咱们会在客户端建立多个线程依次链接服务端并向其发送"当前时间+:hello world",服务端会为每一个客户端线程建立一个线程来处理。代码示例出自闪电侠的博客,原地址以下:

https://www.jianshu.com/p/a4e03835921a

客户端

 
 
  1. /**

  2. *

  3. * @author 闪电侠

  4. * @date 2018年10月14日

  5. * @Description:客户端

  6. */

  7. public class IOClient {


  8.    public static void main(String[] args) {

  9.        // TODO 建立多个线程,模拟多个客户端链接服务端

  10.        new Thread(() -> {

  11.            try {

  12.                Socket socket = new Socket("127.0.0.1", 3333);

  13.                while (true) {

  14.                    try {

  15.                        socket.getOutputStream().write((new Date() + ": hello world").getBytes());

  16.                        Thread.sleep(2000);

  17.                    } catch (Exception e) {

  18.                    }

  19.                }

  20.            } catch (IOException e) {

  21.            }

  22.        }).start();


  23.    }


  24. }

服务端

 
 
  1. /**

  2. * @author 闪电侠

  3. * @date 2018年10月14日

  4. * @Description: 服务端

  5. */

  6. public class IOServer {


  7.    public static void main(String[] args) throws IOException {

  8.        // TODO 服务端处理客户端链接请求

  9.        ServerSocket serverSocket = new ServerSocket(3333);


  10.        // 接收到客户端链接请求以后为每一个客户端建立一个新的线程进行链路处理

  11.        new Thread(() -> {

  12.            while (true) {

  13.                try {

  14.                    // 阻塞方法获取新的链接

  15.                    Socket socket = serverSocket.accept();


  16.                    // 每个新的链接都建立一个线程,负责读取数据

  17.                    new Thread(() -> {

  18.                        try {

  19.                            int len;

  20.                            byte[] data = new byte[1024];

  21.                            InputStream inputStream = socket.getInputStream();

  22.                            // 按字节流方式读取数据

  23.                            while ((len = inputStream.read(data)) != -1) {

  24.                                System.out.println(new String(data, 0, len));

  25.                            }

  26.                        } catch (IOException e) {

  27.                        }

  28.                    }).start();


  29.                } catch (IOException e) {

  30.                }


  31.            }

  32.        }).start();


  33.    }


  34. }

1.4 总结

在活动链接数不是特别高(小于单机1000)的状况下,这种模型是比较不错的,可让每个链接专一于本身的 I/O 而且编程模型简单,也不用过多考虑系统的过载、限流等问题。线程池自己就是一个自然的漏斗,能够缓冲一些系统处理不了的链接或请求。可是,当面对十万甚至百万级链接的时候,传统的 BIO 模型是无能为力的。所以,咱们须要一种更高效的 I/O 处理模型来应对更高的并发量。

2. NIO (New I/O)

2.1 NIO 简介

NIO是一种同步非阻塞的I/O模型,在Java 1.4 中引入了NIO框架,对应 java.nio 包,提供了 Channel , Selector,Buffer等抽象。

NIO中的N能够理解为Non-blocking,不单纯是New。它支持面向缓冲的,基于通道的I/O操做方法。 NIO提供了与传统BIO模型中的 SocketServerSocket 相对应的 SocketChannelServerSocketChannel 两种不一样的套接字通道实现,两种通道都支持阻塞和非阻塞两种模式。阻塞模式使用就像传统中的支持同样,比较简单,可是性能和可靠性都很差;非阻塞模式正好与之相反。对于低负载、低并发的应用程序,可使用同步阻塞I/O来提高开发速率和更好的维护性;对于高负载、高并发的(网络)应用,应使用 NIO 的非阻塞模式来开发。

2.2 NIO的特性/NIO与IO区别

若是是在面试中回答这个问题,我以为首先确定要从 NIO 流是非阻塞 IO 而 IO 流是阻塞 IO 提及。而后,能够从 NIO 的3个核心组件/特性为 NIO 带来的一些改进来分析。若是,你把这些都回答上了我以为你对于 NIO 就有了更为深刻一点的认识,面试官问到你这个问题,你也能很轻松的回答上来了。

1)Non-blocking IO(非阻塞IO)

IO流是阻塞的,NIO流是不阻塞的。

Java NIO使咱们能够进行非阻塞IO操做。好比说,单线程中从通道读取数据到buffer,同时能够继续作别的事情,当数据读取到buffer中后,线程再继续处理数据。写数据也是同样的。另外,非阻塞写也是如此。一个线程请求写入一些数据到某通道,但不须要等待它彻底写入,这个线程同时能够去作别的事情。

Java IO的各类流是阻塞的。这意味着,当一个线程调用 read()write() 时,该线程被阻塞,直到有一些数据被读取,或数据彻底写入。该线程在此期间不能再干任何事情了

2)Buffer(缓冲区)

IO 面向流(Stream oriented),而 NIO 面向缓冲区(Buffer oriented)。

Buffer是一个对象,它包含一些要写入或者要读出的数据。在NIO类库中加入Buffer对象,体现了新库与原I/O的一个重要区别。在面向流的I/O中·能够将数据直接写入或者将数据直接读到 Stream 对象中。虽然 Stream 中也有 Buffer 开头的扩展类,但只是流的包装类,仍是从流读到缓冲区,而 NIO 倒是直接读到 Buffer 中进行操做。

在NIO厍中,全部数据都是用缓冲区处理的。在读取数据时,它是直接读到缓冲区中的; 在写入数据时,写入到缓冲区中。任什么时候候访问NIO中的数据,都是经过缓冲区进行操做。

最经常使用的缓冲区是 ByteBuffer,一个 ByteBuffer 提供了一组功能用于操做 byte 数组。除了ByteBuffer,还有其余的一些缓冲区,事实上,每一种Java基本类型(除了Boolean类型)都对应有一种缓冲区。

3)Channel (通道)

NIO 经过Channel(通道) 进行读写。

通道是双向的,可读也可写,而流的读写是单向的。不管读写,通道只能和Buffer交互。由于 Buffer,通道能够异步地读写。

4)Selectors(选择器)

NIO有选择器,而IO没有。

选择器用于使用单个线程处理多个通道。所以,它须要较少的线程来处理这些通道。线程之间的切换对于操做系统来讲是昂贵的。 所以,为了提升系统效率选择器是有用的。

图片

2.3 NIO 读数据和写数据方式

一般来讲NIO中的全部IO都是从 Channel(通道) 开始的。

  • 从通道进行数据读取 :建立一个缓冲区,而后请求通道读取数据。

  • 从通道进行数据写入 :建立一个缓冲区,填充数据,并要求通道写入数据。

数据读取和写入操做图示:

图片

2.4 NIO核心组件简单介绍

NIO 包含下面几个核心的组件:

  • Channel(通道)

  • Buffer(缓冲区)

  • Selector(选择器)

整个NIO体系包含的类远远不止这三个,只能说这三个是NIO体系的“核心API”。咱们上面已经对这三个概念进行了基本的阐述,这里就很少作解释了。

2.5 代码示例

代码示例出自闪电侠的博客,原地址以下:

https://www.jianshu.com/p/a4e03835921a

客户端 IOClient.java 的代码不变,咱们对服务端使用 NIO 进行改造。如下代码较多并且逻辑比较复杂,你们看看就好。

 
 
  1. /**

  2. *

  3. * @author 闪电侠

  4. * @date 2019年2月21日

  5. * @Description: NIO 改造后的服务端

  6. */

  7. public class NIOServer {

  8.    public static void main(String[] args) throws IOException {

  9.        // 1. serverSelector负责轮询是否有新的链接,服务端监测到新的链接以后,再也不建立一个新的线程,

  10.        // 而是直接将新链接绑定到clientSelector上,这样就不用 IO 模型中 1w 个 while 循环在死等

  11.        Selector serverSelector = Selector.open();

  12.        // 2. clientSelector负责轮询链接是否有数据可读

  13.        Selector clientSelector = Selector.open();


  14.        new Thread(() -> {

  15.            try {

  16.                // 对应IO编程中服务端启动

  17.                ServerSocketChannel listenerChannel = ServerSocketChannel.open();

  18.                listenerChannel.socket().bind(new InetSocketAddress(3333));

  19.                listenerChannel.configureBlocking(false);

  20.                listenerChannel.register(serverSelector, SelectionKey.OP_ACCEPT);


  21.                while (true) {

  22.                    // 监测是否有新的链接,这里的1指的是阻塞的时间为 1ms

  23.                    if (serverSelector.select(1) > 0) {

  24.                        Set<SelectionKey> set = serverSelector.selectedKeys();

  25.                        Iterator<SelectionKey> keyIterator = set.iterator();


  26.                        while (keyIterator.hasNext()) {

  27.                            SelectionKey key = keyIterator.next();


  28.                            if (key.isAcceptable()) {

  29.                                try {

  30.                                    // (1)

  31.                                    // 每来一个新链接,不须要建立一个线程,而是直接注册到clientSelector

  32.                                    SocketChannel clientChannel = ((ServerSocketChannel) key.channel()).accept();

  33.                                    clientChannel.configureBlocking(false);

  34.                                    clientChannel.register(clientSelector, SelectionKey.OP_READ);

  35.                                } finally {

  36.                                    keyIterator.remove();

  37.                                }

  38.                            }


  39.                        }

  40.                    }

  41.                }

  42.            } catch (IOException ignored) {

  43.            }

  44.        }).start();

  45.        new Thread(() -> {

  46.            try {

  47.                while (true) {

  48.                    // (2) 批量轮询是否有哪些链接有数据可读,这里的1指的是阻塞的时间为 1ms

  49.                    if (clientSelector.select(1) > 0) {

  50.                        Set<SelectionKey> set = clientSelector.selectedKeys();

  51.                        Iterator<SelectionKey> keyIterator = set.iterator();


  52.                        while (keyIterator.hasNext()) {

  53.                            SelectionKey key = keyIterator.next();


  54.                            if (key.isReadable()) {

  55.                                try {

  56.                                    SocketChannel clientChannel = (SocketChannel) key.channel();

  57.                                    ByteBuffer byteBuffer = ByteBuffer.allocate(1024);

  58.                                    // (3) 面向 Buffer

  59.                                    clientChannel.read(byteBuffer);

  60.                                    byteBuffer.flip();

  61.                                    System.out.println(

  62.                                            Charset.defaultCharset().newDecoder().decode(byteBuffer).toString());

  63.                                } finally {

  64.                                    keyIterator.remove();

  65.                                    key.interestOps(SelectionKey.OP_READ);

  66.                                }

  67.                            }


  68.                        }

  69.                    }

  70.                }

  71.            } catch (IOException ignored) {

  72.            }

  73.        }).start();


  74.    }

  75. }

为何你们都不肯意用 JDK 原生 NIO 进行开发呢?从上面的代码中你们均可以看出来,是真的难用!除了编程复杂、编程模型难以外,它还有如下让人诟病的问题:

  • JDK 的 NIO 底层由 epoll 实现,该实现饱受诟病的空轮询 bug 会致使 cpu 飙升 100%

  • 项目庞大以后,自行实现的 NIO 很容易出现各种 bug,维护成本较高,上面这一坨代码我都不能保证没有 bug

Netty 的出现很大程度上改善了 JDK 原生 NIO 所存在的一些让人难以忍受的问题。

3. AIO (Asynchronous I/O)

AIO 也就是 NIO 2。在 Java 7 中引入了 NIO 的改进版 NIO 2,它是异步非阻塞的IO模型。异步 IO 是基于事件和回调机制实现的,也就是应用操做以后会直接返回,不会堵塞在那里,当后台处理完成,操做系统会通知相应的线程进行后续的操做。

AIO 是异步IO的缩写,虽然 NIO 在网络操做中,提供了非阻塞的方法,可是 NIO 的 IO 行为仍是同步的。对于 NIO 来讲,咱们的业务线程是在 IO 操做准备好时,获得通知,接着就由这个线程自行进行 IO 操做,IO操做自己是同步的。(除了 AIO 其余的 IO 类型都是同步的,这一点能够从底层IO线程模型解释,推荐一篇文章:《漫话:如何给女友解释什么是Linux的五种IO模型?》

查阅网上相关资料,我发现就目前来讲 AIO 的应用还不是很普遍,Netty 以前也尝试使用过 AIO,不过又放弃了。

参考

  • 《Netty 权威指南》第二版

  • https://zhuanlan.zhihu.com/p/23488863 (美团技术团队)

转自公众号: JavaGuide

相关文章
相关标签/搜索