01-IO模型(BIO、NIO、AIO)

本文详细介绍了Java中的五种IO模型:阻塞IO、非阻塞IO、I/O多路复用、信号驱动IO和异步IO。重点讨论了BIO、NIO(New I/O)和AIO(Asynchronous I/O)的差异,包括同步与异步、阻塞与非阻塞的概念。BIO是同步阻塞模型,适合连接数目小且固定的场景;NIO提供非阻塞IO,适用于高并发、短连接的场景;AIO则是异步非阻塞,适用于长连接和高并发操作。文章还包含了BIO和NIO的代码示例,以及对各种IO模型适用场景的分析。
摘要由CSDN通过智能技术生成

文章放置于:https://github.com/zgkaii/CS-Study-Notes,欢迎批评指正!

一、IO模型分类

首先明确两个概念:用户空间和内核空间

操作系统的核心是内核(kernel),它独立于普通的应用程序,可以访问受保护的内存空间,也有访问底层硬件设备的所有权限。为了保证内核的安全,现在的操作系统一般都强制用户进程不能直接操作内核。具体的实现方式基本都是由操作系统将虚拟地址空间划分为两部分,一部分为内核空间,另一部分为用户空间。

其实所有的系统资源管理都是在内核空间中完成的。比如读写磁盘文件,分配回收内存,从网络接口读写数据等等。我们的应用程序是无法直接进行这样的操作的。但是我们可以通过内核提供的接口来完成这样的任务。

所以,当我们使用 TCP 发送数据的时候,需要先将数据从用户空间拷贝到内核空间,再由内核操作将数据从内核空间发送出去;当我们使用 TCP 读取数据的时候,数据先在内核空间准备好,再从内核空间拷贝到用户空间供用户进程使用。

	客户端 --1.发送请求-> 网关 --2.拷贝-> 内核空间 --3.拷贝-> 用户空间
															 |
	客户端	<--7.响应-- 网关 <--6.拷贝-- 内核空间 <--5.拷贝-- 用户空间(4.用户进程处理数据)				 

所以,一次 IO 的读取操作分为两个阶段(写入操作类似):

  • 等待内核空间准备数据
  • 数据从内核空间拷贝到用户空间

在这基础之上,Unix 把 IO 分成了以下五种 IO 模型:

  • 阻塞型 IO
  • 非阻塞型 IO
  • IO 多路复用
  • 信号驱动 IO
  • 异步 IO

1.1 阻塞型 IO

阻塞型 IO,即当用户进程发起请求时,一直阻塞直到数据拷贝到用户空间为止才返回(两个阶段都阻塞)。

在阻塞的过程中,其它应用进程还可以执行,因此阻塞不意味着整个操作系统都被阻塞。因为其它应用进程还可以执行,所以不消耗 CPU 时间,这种模型的 CPU 利用率会比较高。

1.2 非阻塞型 IO

和阻塞 IO 类比,无论内核空间数据是否准备好,非阻塞型 IO内核都会立即返回,返回后获得足够的 CPU 时间继续做其它的事情。 用户进程第一个阶段不是阻塞的,需要不断的主动询问 kernel 数据好了没有(轮询polling);第二个阶段依然总是阻塞的。

由于 CPU 要处理更多的系统调用,因此这种模型的 CPU 利用率比较低。

1.3 I/O 多路复用

IO 多路复用(IO multiplexing),也称事件驱动 IO(event-driven IO),就是在单个线程里同时监控多个套接字,通过 select 或 poll 轮询所负责的所有 socket,当某个 socket 有数据到达了,就通知用户进程。
在这里插入图片描述

IO 复用同非阻塞 IO 本质一样,不过利用 了新的 select 系统调用,由内核来负责本 来是请求进程该做的轮询操作。看似比非阻塞 IO 还多了一个系统调用开销,不过因为可以支持多路 IO,才算提高了效率。

进程先是阻塞在 select/poll 上,再是阻塞在读操作的第二个阶段上。

select/poll 的几大缺点:

(1)每次调用 select,都需要把 fd 集合从用户态拷贝到 内核态,这个开销在 fd 很多时会很大 。

(2)同时每次调用 select 都需要在内核遍历传递进来的 所有 fd,这个开销在 fd 很多时也很大 。

(3)select 支持的文件描述符数量太小了,默认是1024 。

epoll(Linux 2.5.44内核中引入,2.6内核正式引入,可被用 于代替 POSIX select 和 poll 系统调用):

(1)内核与用户空间共享一块内存。

(2)通过回调解决遍历问题 。

(3)fd 没有限制,可以支撑10万连接。

1.4 信号驱动 I/O

信号驱动 IO 与 BIO 和 NIO 最大的区别就在 于,在 IO 执行的数据准备阶段,不需要轮询。

当用户进程需要等待数据的时候 ,会向内核发送一个信号,告诉内核我要什么数据,然后用户进程就继续做别的事情去 了,而当内核中的数据准备好之后,内核立马发给用户进程一个信号,说”数据准备好 了,快来查收“,用户进程收到信号之后, 立马调用 recvfrom,去查收数据。

相比于非阻塞式I/O的轮询方式,信号驱动 I/O 的CPU利用率更高。
在这里插入图片描述

1.5 异步 I/O

异步 IO 真正实现了 IO 全流程的非阻塞。 用户进程发出系统调用后立即返回,内核等待数据准备完成,然后将数据拷贝到用 户进程缓冲区,然后发送信号告诉用户进 程 IO 操作执行完毕(与 SIGIO 相比,一 个是发送信号告诉用户进程数据准备完毕, 一个是 IO执行完毕)。

1.6 五大 I/O 模型比较

  • 同步 I/O:将数据从内核缓冲区复制到应用进程缓冲区的阶段(第二阶段),应用进程会阻塞。
  • 异步 I/O:第二阶段应用进程不会阻塞。

同步 I/O 包括阻塞式 I/O、非阻塞式 I/O、I/O 复用和信号驱动 I/O ,它们的主要区别在第一个阶段。

非阻塞式 I/O 、信号驱动 I/O 和异步 I/O 在第一阶段不会阻塞。

二、BIO、NIO与AIO

先来回顾一下这样几个概念:同步与异步,阻塞与非阻塞。参考 Stackoverflow相关问题的回答:

When you execute something synchronously, you wait for it to finish before moving on to another task. When you execute something asynchronously, you can move on to another task before it finishes.

当你同步执行某项任务时,你需要等待其完成才能继续执行其他任务。当你异步执行某些操作时,你可以在完成另一个任务之前继续进行。

同步与异步

  • 同步 :两个同步任务相互依赖,并且一个任务必须以依赖于另一任务的某种方式执行。 比如在A->B事件模型中,你需要先完成 A 才能执行B。 再换句话说,同步调用中被调用者未处理完请求之前,调用不返回,调用者会一直等待结果的返回。
  • 异步: 两个异步的任务完全独立的,一方的执行不需要等待另外一方的执行。再换句话说,异步调用种一调用就返回结果不需要等待结果返回,当结果返回的时候通过回调函数或者其他方式拿着结果再做相关事情,

阻塞和非阻塞

  • 阻塞: 阻塞就是发起一个请求,调用者一直等待请求结果返回,也就是当前线程会被挂起,无法从事其他任务,只有当条件就绪才能继续。
  • 非阻塞: 非阻塞就是发起一个请求,调用者不用一直等着结果返回,可以先去干其他事情。

如何区分 “同步/异步 ”和 “阻塞/非阻塞” 呢?

同步/异步是从行为角度描述事物的,而阻塞和非阻塞描述的当前事物的状态(等待调用结果时的状态)。

BIO、NIO与AIO:

(1)BIO (Blocking I/O),阻塞型 IO,也称为 OIO,Old IO。

(2)NIO(New IO也可以理解为No-Locking IO),Java 中使用 IO 多路复用技术实现,放在 java.nio 包下,JDK1.4 引入。

(3)AIO(Asynchronous I/O),异步 IO,又称为 NIO2,也是放在 java.nio 包下,JDK1.7 引入。

2.1 BIO

BIO采用同步阻塞模型,当用户进程发起请求时,一直阻塞到数据拷贝到用户空间为止。(Java BIO 就是传统I/O编程,其相关的类和接口都在 java.io包下)

2.1.1 传统 BIO

BIO通信(一请求一应答)模型图如下:

传统BIO通信模型图

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

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

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

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

2.1.2 伪异步 IO

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

伪异步IO模型图:

伪异步IO模型图

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

伪异步I/O通信框架采用了线程池实现,因此避免了为每个请求都创建一个独立线程造成的线程资源耗尽问题。不过因为它的底层仍然是同步阻塞的BIO模型,因此无法从根本上解决问题。

1.3 代码示例

下面代码中演示了BIO通信(一请求一应答)模型。我们会在客户端创建多个线程依次连接服务端并向其发送"当前时间+:hello world",服务端会为每个客户端线程创建一个线程来处理。

客户端

public class IOClient {
   
    public static void main(String[] args) {
   
        new Thread(() -> {
   
            try {
   
                Socket socket = new Socket("127.0.0.1", 6666);
                while (true) {
   
                    try {
   
                        socket.getOutputStream()
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值