• 新版网站前后台即将上线,2019年将致力于提高文章质量,加大原创力度,打造一个更加舒适的阅读体验!
  • 极客文库小编@勤劳的小蚂蚁,为您推荐每日资讯,欢迎关注!
  • 新版网站前后台即将上线,2019年将致力于提高文章质量,加大原创力度,打造一个更加舒适的阅读体验!
  • 如果有任何体验不佳的地方,欢迎向客服反馈!

详解 Java 中 4 种 I/O 模型

同步、异步、阻塞、非阻塞都是和 I/O(输入输出)有关的概念,最简单的文件读取就是 I/O 操作。而在文件读取这件事儿上,可以有多种方式。

本篇会先介绍一下 I/O 的基本概念,通过一个生活例子来分别解释下这几种 I/O 模型,以及 Java 支持的 I/O 模型。

基本概念

在解释 I/O 模型之前,我先说明一下几个操作系统的概念

文件描述符 fd

文件描述符(file descriptor)是计算机科学中的一个术语,是一个用于表述指向文件的引用的抽象化概念。

文件描述符在形式上是一个非负整数。实际上,它是一个索引值,指向内核为每一个进程所维护的该进程打开文件的记录表。 当程序打开一个现有文件或者创建一个新文件时,内核向进程返回一个文件描述符。 在程序设计中,一些涉及底层的程序编写往往会围绕着文件描述符展开。文件描述符这一概念往往只适用于 UNIX、Linux 这样的操作系统。

缓存 I/O

缓存 I/O 又被称作标准 I/O,大多数文件系统的默认 I/O 操作都是缓存 I/O。在 Linux 的缓存 I/O 机制中, 操作系统会将 I/O 的数据缓存在文件系统的页缓存中,也就是说,数据会先被拷贝到操作系统内核的缓冲区中, 然后才会从操作系统内核的缓冲区拷贝到应用程序的地址空间。

缓存 I/O 的缺点是数据在传输过程中需要在应用程序地址空间和内核进行多次数据拷贝操作,这些数据拷贝操作所带来的 CPU 以及内存开销是非常大的。

下面我以一个生活中烧开水的例子来形象解释一下同步、异步、阻塞、非阻塞概念。

同步和异步

说到烧水,我们都是通过热水壶来烧水的。在很久之前,科技还没有这么发达的时候,如果我们要烧水, 需要把水壶放到火炉上,我们通过观察水壶内的水的沸腾程度来判断水有没有烧开。

随着科技的发展,现在市面上的水壶都有了提醒功能,当我们把水壶插电之后,水壶水烧开之后会通过声音提醒我们水开了。

对于烧水这件事儿来说,传统水壶的烧水就是同步的,高科技水壶的烧水就是异步的。

同步请求

A 调用 B,B 的处理是同步的,在处理完之前他不会通知 A,只有处理完之后才会明确的通知 A。

异步请求

A 调用 B,B 的处理是异步的,B 在接到请求后先告诉 A 我已经接到请求了,然后异步去处理,处理完之后通过回调等方式再通知 A。

所以说,同步和异步最大的区别就是被调用方的执行方式和返回时机。 同步指的是被调用方做完事情之后再返回,异步指的是被调用方先返回,然后再做事情,做完之后再想办法通知调用方。

阻塞和非阻塞

还是那个烧水的例子,当你把水放到水壶里面,按下开关后,你可以坐在水壶前面,别的事情什么都不做, 一直等着水烧好。你还可以先去客厅看电视,等着水开就好了。

对于你来说,坐在水壶前面等就是阻塞的,去客厅看电视等着水开就是非阻塞的。

阻塞请求

A 调用 B,A 一直等着 B 的返回,别的事情什么也不干。

非阻塞请求

A 调用 B,A 不用一直等着 B 的返回,先去忙别的事情了。

所以说,阻塞和非阻塞最大的区别就是在被调用方返回结果之前的这段时间内,调用方是否一直等待。 阻塞指的是调用方一直等待别的事情什么都不做。非阻塞指的是调用方先去忙别的事情。

阻塞、非阻塞和同步、异步的区别

首先,前面已经提到过,阻塞、非阻塞和同步、异步其实针对的对象是不一样的。

给我大声念三遍下面的句子

阻塞、非阻塞说的是调用者。同步、异步说的是被调用者。

阻塞、非阻塞说的是调用者。同步、异步说的是被调用者。

阻塞、非阻塞说的是调用者。同步、异步说的是被调用者。

有人认为阻塞和同步是一回事儿,非阻塞和异步是一回事。但是这是不对的。

同步包含阻塞和非阻塞

我们是用传统的水壶烧水。在水烧开之前我们一直做在水壶前面,等着水开。这就是阻塞的。

我们是用传统的水壶烧水。在水烧开之前我们先去客厅看电视了,但是水壶不会主动通知我们, 需要我们时不时的去厨房看一下水有没有烧开,这就是非阻塞的。

异步包含阻塞和非阻塞

我们是用带有提醒功能的水壶烧水。在水烧发出提醒之前我们一直做在水壶前面,等着水开。这就是阻塞的。

我们是用带有提醒功能的水壶烧水。在水烧发出提醒之前我们先去客厅看电视了,等水壶发出声音提醒我们。这就是非阻塞的。推荐阅读:46 道阿里巴巴 Java 面试题,你会几道?

Unix 中的五种 I/O 模型

对于一次 I/O 访问(以 read 举例),数据会先被拷贝到操作系统内核的缓冲区中,然后才会从操作系统内核的缓冲区拷贝到应用程序的地址空间。 所以说,当一个 read 操作发生时,它会经历两个阶段:

第一阶段:等待数据准备 (Waiting for the data to be ready)。

第二阶段:将数据从内核拷贝到进程中 (Copying the data from the kernel to the process)。

对于 socket 流而言

第一阶段:通常涉及等待网络上的数据分组到达,也就是被复制到内核的某个缓冲区。

第二阶段:把数据从内核缓冲区复制到应用进程缓冲区。

Unix 下五种 I/O 模型:

  1. 同步阻塞 I/O

  2. 同步非阻塞 I/O

  3. I/O 多路复用(select 和 poll)

  4. 信号驱动 I/O(SIGIO)

  5. 异步非阻塞 IO

同步阻塞 I/O

阻塞 I/O 下请求无法立即完成则保持阻塞,阻塞 I/O 分为如下两个阶段。

阶段 1:等待数据就绪。网络 I/O 的情况就是等待远端数据陆续抵达,也就是网络数据被复制到内核缓存区中,磁盘 I/O 的情况就是等待磁盘数据从磁盘上读取到内核态内存中。

阶段 2:数据拷贝。出于系统安全,用户态的程序没有权限直接读取内核态内存,因此内核负责把内核态内存中的数据拷贝一份到用户态内存中。

这两个阶段必须都完成后才能继续下一步操作

所以,blocking IO的特点就是在IO执行的两个阶段都被 block 了。

同步非阻塞 I/O

就是阶段 1 的时候用户进程可选择做其他事情,通过轮询的方式看看内核缓冲区是否就绪。如果数据就绪,再去执行阶段 2。

也就是说非阻塞的 recvform 系统调用调用之后,进程并没有被阻塞,内核马上返回给进程,如果数据还没准备好, 此时会返回一个 error。进程在返回之后,可以干点别的事情,然后再发起 recvform 系统调用。

重复上面的过程, 循环往复的进行 recvform 系统调用。这个过程通常被称之为轮询。轮询检查内核数据,直到数据准备好, 再拷贝数据到进程,进行数据处理。需要注意,第 2 阶段的拷贝数据整个过程,进程仍然是属于阻塞的状态。推荐阅读:Java 8 开发的 4 大顶级技巧

在 linux 下,可以通过设置 socket 使其变为 non-blocking。当对一个 non-blocking socket 执行读操作时,流程如图所示:

所以,nonblocking IO 的特点是用户进程需要不断的主动询问 kernel 数据好了没有。

I/O 多路复用

我这里只想重点解释一下 I/O 多路复用这种模型,因为现在用的最多。很多地方也称为事件驱动IO模型,只是叫法不同,意思都一个样。

IO多路复用是指内核一旦发现进程指定的一个或者多个IO条件准备读取,它就通知该进程。

目前支持 I/O 多路复用的系统调用有 select、pselect、poll、epoll,I/O 多路复用就是通过一种机制,一个进程可以监视多个描述符, 一旦某个文件描述符 fd 就绪(一般是读就绪或者写就绪),能够通知程序进行相应的读写操作。 

但 select、pselect、poll、epoll 本质上都是同步 I/O,因为他们都需要在读写事件就绪后自己负责进行读写,也就是说这个读写过程是阻塞的, 而异步 I/O 则无需自己负责进行读写,异步 I/O 的实现会负责把数据从内核拷贝到用户空间。

相比较于同步非阻塞 I/O,它的改进的地方在于,原来需要用户进程去轮询的这事儿交给了内核线程帮你完成, 而且这个内核线程可以等待多个 socket,能实现同时对多个IO端口进行监听。

多路复用的特点是通过一种机制一个进程能同时等待 IO 文件描述符,内核监视这些文件描述符(套接字描述符), 其中的任意一个进入读就绪状态,select, poll,epoll 函数就可以返回。对于监视的方式, 又可以分为 select, poll, epoll 三种方式。

所以,如果处理的连接数不是很高的话,使用 select/epoll 的 web server 不一定比使用多线程 + 阻塞 IO 的 web server 性能更好,可能延迟还更大。 也就是说,select/epoll 的优势并不是对于单个连接能处理得更快,而是在于能处理更多的连接。推荐阅读:Spring Boot 的 10 个核心模块

高并发的程序一般使用同步非阻塞方式而非多线程 + 同步阻塞方式。要理解这一点,首先要扯到并发和并行的区别。 比如去某部门办事需要依次去几个窗口,办事大厅里的人数就是并发数,而窗口个数就是并行度。 也就是说并发数是指同时进行的任务数(如同时服务的 HTTP 请求),而并行数是可以同时工作的物理资源数量(如 CPU 核数)。 

通过合理调度任务的不同阶段,并发数可以远远大于并行度,这就是区区几个 CPU 可以支持上万个用户并发请求的奥秘。 在这种高并发的情况下,为每个任务(用户请求)创建一个进程或线程的开销非常大。而同步非阻塞方式可以把多个 IO 请求丢到后台去, 这就可以在一个进程里服务大量的并发 IO 请求。

IO 多路复用归为同步阻塞模式

异步非阻塞 IO

相对于同步 IO,异步 IO 不是顺序执行。用户进程进行 aio_read 系统调用之后,无论内核数据是否准备好,都会直接返回给用户进程, 然后用户态进程可以去做别的事情。等到 socket 数据准备好了,内核直接复制数据给进程,然后从内核向进程发送通知。IO 两个阶段, 进程都是非阻塞的。

Linux 提供了 AIO 库函数实现异步,但是用的很少。目前有很多开源的异步 IO 库,例如 libevent、libev、libuv。异步过程如下图所示:

更详细的分析可参考 聊聊 Linux5 种 IO 模型

Java 中四种 I/O 模型

上一章所述 Unix 中的五种 I/O 模型,除信号驱动 I/O 外,Java 对其它四种 I/O 模型都有所支持。

  1. Java 传统 IO 模型即是同步阻塞 I/O

  2. NIO是同步非阻塞 I/O

  3. 通过 NIO 实现的 Reactor 模式即是 I/O 多路复用模型的实现

  4. 通过 AIO 实现的 Proactor 模式即是异步 I/O 模型的实现


丨极客文库, 版权所有丨如未注明 , 均为原创丨
本网站采用知识共享署名-非商业性使用-相同方式共享 3.0 中国大陆许可协议进行授权
转载请注明原文链接:详解 Java 中 4 种 I/O 模型
喜欢 (0)
[247507792@qq.com]
分享 (0)
勤劳的小蚂蚁
关于作者:
温馨提示:本文来源于网络,转载文章皆标明了出处,如果您发现侵权文章,请及时向站长反馈删除。

欢迎 注册账号 登录 发表评论!

  • 精品技术教程
  • 编程资源分享
  • 问答交流社区
  • 极客文库知识库

客服QQ


QQ:2248886839


工作时间:09:00-23:00