Java I/O简介

Posted by LiuXi on 2017-10-23

Java I/O 是 Java SE 中比较重要的一部分内容,但对于阻塞非阻塞、同步异步的各种组合说法,有时会造成误解与困扰。

比如:同步I/O与阻塞I/O是一个含义么?同步阻塞I/O、同步非阻塞I/O又是什么含义呢?Java都支持哪些呢?

1 I/O调用

1.1 背景

既然是要梳理Java I/O,讨论的背景就主要是Java 支持的I/O类型,例如:文件读写、网络I/O等,本篇已java网络编程中的socket为背景阐述。

1.2 参与者

在Java I/O应用场景中,涉及三个参与者:

  • 用户进程:运行时JVM进程(本文中的用户进程只是作为应用程序的角色来说的,并不是作为操作系统进程这个角色来说,不要误解用户进程与下面提及的线程,用户进程作为一个参与角色,并没有进程包含线程的含义)
  • 内核:操作系统内核
  • 硬件

1.3 两个步骤

以I/O读操作为例,用户进程想要完成读操作,需要通过系统调用,由操作系统内核读取硬件上的数据到内核空间,然后这部分读取的数据由内核空间复制到用户空间,至此,用户进程就可以使用读取的数据。

从上可以看出,一次读操作有两个步骤(后面五种I/O模型就是针对这两个步骤的各种实现方式):

  1. 内核准备数据
  2. 数据从内核空间复制到用户空间

下面看下内核空间和用户空间的区别:

  • 内核空间:操作系统所在区域,属于特权区域,如能直接访问硬件
  • 用户空间:用户进程所在的区域,属于非特权区域,执行I/O操作时,通过执行系统调用将控制权交由内核

ps: 操作系统内核可以理解成一个中间层,用户进程是应用层,硬件是底层。中间层对应用层提供对底层访问的能力。

2 五种IO模型

暂且搁置文初提出的同步异步、阻塞非阻塞这些概念,来看下《Unix网络编程》中介绍的五种I/O模型处理I/O read操作的处理方式

五种I/O模型主要是在上述1.2小节中所述的I/O操作的两个步骤(准备数据和复制数据)的处理方式上有所不同

2.1 阻塞I/O

用户进程需要进行I/O操作时,发起系统调用recvfrom

  • 准备数据:内核准备数据(例如:对于磁盘I/O,内核会进行磁盘操作),将数据加载到内核空间中
  • 复制数据:数据准备好之后,数据从内核空间复制到用户空间,此时对与用户线程来说,I/O操作完成

结合上图和两个步骤可以看出,用户进程在两个步骤中都会处于等待状态,一直被阻塞,所以称之为阻塞I/O

2.2 非阻塞I/O

用户进程需要进行I/O操作时,发起系统调用recvfrom

  • 准备数据:内核准备数据,将数据加载到内核空间中。若数据尚未准备好,内核会对系统调用recvfrom返回一个错误
  • 复制数据:用户进程发起的系统调用recvfrom,在内核数据准备好之后,数据从内核空间复制到用户空间,I/O操作完成

结合上图和两个步骤可以看出,用户进程在步骤一上并不会被阻塞(但是用户进程需要一致轮询数据是否准备好),只有在步骤二时用户进程才会处于等待状态,所以称之为非阻塞I/O

2.3 I/O多路复用

用户进程需要进行I/O操作时,发起系统调用select

  • 准备数据:用户进程发起系统调用select之后,内核会监听与当前select相关的socket(一般是多个),直到有socket的数据准备好才返回
  • 复制数据:用户进程发起的系统调用recvfrom,数据从内核空间复制到用户空间,I/O操作完成

结合上图和两个步骤可以看出,用户进程在步骤一上发起系统调用select,在有数据准备好之前会一直处于等待状态,步骤二也会处于等待状态。

这种I/O模型虽然在两个步骤都会阻塞,但是相对于前两种来说,他能同时处理多个socket,所以称之为I/O多路复用

ps:对于用户进程来说,处理多个socket,一般的处理方式可以使用一个单独的线程来完成步骤一操作,轮询内核在有socket准备好数据之后,通知其他线程进行步骤二操作,这样对于大量的socket的I/O操作管理来说,处理效率比较高。这样一种以事件驱动来处理I/O事件的多线程模型,其实就是Reactor线程模型

2.4 信号驱动I/O

并不常用,暂略

2.5 异步I/O

用户进程需要进行I/O操作,发起系统调用aio_read,同时制定I/O操作完成后的回调处理函数

  • 准备数据:内核准备数据,将数据加载到内核空间中
  • 复制数据:内核在数据准备好后,将数据从内核空间复制到用户空间,完成之后触发回调函数

结合上图和两个步骤可以看出,用户进程在提交了I/O操作之后,其余的事情交由内核处理(包括I/O操作完成后,回调函数的触发也不需要用户进程参与),所以称之为异步I/O

2.6 总结

从上面五种I/O模型可看出,I/O模型的实现方式依赖于操作系统对于I/O操作系统调用命令的支持方式。五种I/O模型可以理解为一种逐渐迭代、进化的模型。

  • 阻塞I/O:对于用户进程和操作系统来说都比较简单,用户不用考虑多线程和异步相关的问题,初期应用场景可能也比较简单,处理的socket也比较少
  • 非阻塞I/O:阻塞I/O对于用户进程来说不是特别友好,用户进程希望在内核准备数据期间不要一直阻塞等待结果,此时非阻塞I/O就是在内核尚未准备好数据时候,立即回复用户进程数据未准备好,用户进程就可以先处理其他任务,过会再来询问,直到内核准备好数据再来处理数据
  • I/O多路复用:相对于非阻塞I/O的单线程轮询处理单个socket,I/O多路复用支持同时监视多个socket,当有socket准备好数据后,用户线程即可处理数据。这样在使用合适的线程模型的话(Reactor或PReactor线程模型),对于用户进程来说,I/O操作可以是异步化的(这里的异步化其实是伪异步化,因为内核此时还是同步的)
  • 异步I/O:内核支持的真正的异步化I/O操作

3 JAVA I/O实现

上面五种I/O模型主要是操作系统层面的支持,下面了解下JAVA对阻塞I/O、I/O多路复用和异步I/O的实现

3.1 JAVA BIO

JAVA BIO(Blocking I/O):基于流Stream的标准I/O(java.io包下),是对阻塞I/O的实现

3.2 JAVA NIO

JAVA NIO(New I/O):基于块(Buffer)、选择器(Selector)和通道(Channel)的I/O(java.nio包下),是对I/O多路复用的实现

JDK1.4提供了对NIO的支持

3.3 JAVA AIO

JAVA AIO(Asynchronous I/O):也称之为 NIO2(java.nio包下),是对异步I/O的实现

JDK7提供了对AIO的支持

4 阻塞 非阻塞 同步 异步

最后来说明下同步、异步、阻塞、非阻塞的含义

4.1 释义

4.1.1 阻塞 vs 非阻塞

  • 阻塞:用户进程在发起I/O操作到I/O操作完成期间,一直处于等待状态,不能处理其他任务

第2小节中的五种I/O模型中的阻塞I/O属于阻塞I/O

  • 非阻塞I/O:用户进程在发起I/O操作后,内核数据未准备好的情况下,可以处理其他任务

第2小节中的五种I/O模型中的非阻塞I/OI/O多路复用异步I/O都属于非阻塞I/O

Q&A:

Q: 如何理解I/O多路复用是非阻塞I/O呢?select不是会阻塞吗?
A:select操作是会阻塞线程,但是select一般交由单独的线程执行,并不会阻塞调用I/O操作的线程。
且I/O操作的第一步准备数据和第二步复制数据也并不是作为一个整体阻塞线程的,所以不是阻塞I/O

4.1.2 同步 vs 异步

  • 同步I/O:I/O操作未完成会造成用户进程处于等待状态,即用户进程被阻塞;I/O操作完成,用户进程才能处理其他任务

第2小节中的五种I/O模型中的阻塞I/O非阻塞I/OI/O多路复用信号驱动I/O都属于同步I/O

  • 异步I/O:I/O操作是否完成都不会阻塞用户进程;I/O操作完成后,会触发回调函数

第2小节中的五种I/O模型中的异步I/O属于异步I/O

Q&A:

Q: 如何理解非阻塞I/OI/O多路复用是同步的呢?非阻塞I/O不应该是异步的吗?
A:首先,非阻塞和异步不是一个含义。
非阻塞I/O虽然在步骤1准备数据时候,不会阻塞线程,但是步骤2还是会阻塞线程的。而且线程也需要一直执行步骤1进行轮询,所以是同步的
I/O多路复用非阻塞I/O类似,只不过步骤1替换成了阻塞的,所以也是同步的

4.1.3 总结

  • 只有异步I/O是真正异步的(I/O多路复用只能通过线程模型做到伪异步),其余都是同步的
  • 只有阻塞I/O是阻塞的,其余都是非阻塞的
  • 同步和异步强调用户进程的状态,用户进程需要依赖、等待内核完成整个I/O操作才能处理数据,则为同步;而异步I/O,用户进程无需关心内核何时完成,且完成后,数据处理回调函数无需用户进程触发
  • 阻塞和非阻塞强调内核的状态,内核I/O操作完成前一直不回复用户进程,则为阻塞;而非阻塞,内核在准备数据期间,可以支持用户进程处于非阻塞状态,甚至异步I/O的复制数据也由内核主动完成
  • 阻塞、非阻塞、I/O多路复用、异步I/O,这四种I/O模型可以看成是操作系统为了优化用户进程的I/O操作的逐步演进的过程

4.2 示例

最后给出一个例子,对比下各种I/O模型

场景:
用户需要办理一张新银行卡,去银行大厅取号排队吗,待被叫号后,去柜台办理业务,银行职员在银行系统中录入信息,并给用户一张新的银行卡

参与者:

  • 用户(与I/O模型中的用户进程对应)
  • 银行职员(与I/O模型中的内核对应)
  • 银行系统(与I/O模型中的硬件对应)

可以对比看下:银行职员属于银行内部人员,可以看做是特权人员,有权限访问银行系统,而用户很显然没有这种特权

两个步骤:

  • 取号排队:用户去银行大厅取号,排队等候(与I/O模型中的准备数据对应)
  • 办理业务:被叫号后,用户到柜台前办理新卡业务(与I/O模型中的准备数据对应)

I/O模型:

  • 阻塞模型:用户去银行大厅取号排队,被叫号后办理业务,可看出,用户在拿到新银行卡前不能离开银行,需要一直在银行等候
  • 非阻塞模型:银行系统升级,支持用户拨打专属客服电话(一个用户配一个专属客服),取号和询问叫号进度,用户可去办理自己的事情,然后隔一段时间问一下客服,当叫号时间临近,用户在去银行柜台办理业务,可看出,用户只需要在办理业务时去银行柜台等候办理即可,取号排队期间可以先去外面办理自己的事情,但是这种方式一个用户配一个专属客服,这对银行来说成本很大,也不利于服务更多用户
  • I/O多路复用模型:移动互联网时代,银行系统再次升级,开发一个网上取号和进度查询系统,用户可以直接在网上取号并查询叫号进度,与上一种模型不同的是,这个系统能服务所有的用户,但是办理业务时候,用户还是需要去银行柜台
  • 异步模型:移动互联网+O2O时代,银行系统终极升级,开发了一个网上业务办理系统,用户上网录入办理业务信息并填入快递配送地址,银行职员办理好业务后,直接将新卡快递给用户

再来结合例子看看诡异的同步阻塞、同步非阻塞、异步阻塞、异步非阻塞
阻塞模型可看做是同步阻塞
非阻塞模型I/O多路复用模型可看作是同步非阻塞
异步模型可看做是异步非阻塞
至此,异步阻塞是啥?对于异步模型,如果用户上网提交业务办理信息后,仍然跑去银行大厅一直等候,直至拿到新卡,这可称之为异步阻塞(诡异吧~~)

所以:综上所述,I/O模型重点理解阻塞I/O模型、非阻塞I/O模型、I/O多路复用模型和异步I/O模型
而无需过多纠结于同步阻塞、同步非阻塞、异步阻塞、异步非阻塞