介绍本文前要先明确一个概念:Linux的5种IO模型和Java中的3种IO模型严格讲只是有一定联系,二者并不是等价的。学习Java IO模型建议先学习一下Linux的IO模型,有关Linux的5种IO模型可以参考我这篇文章:https://www.yuque.com/docs/share/29acd9d2-6bc2-4994-98f0-3aec42d526f0?# 《Linux的5种IO模型》。

1、概述

前面介绍过操作系统的五种IO模型,这里的Java IO模型跟操作系统的IO模型有什么关联呢?Java中主要有三种IO模型,分别是阻塞IO(BIO)、非阻塞IO(NIO)和异步IO(AIO)。Java中提供的IO有关的API,底层都是依赖于操作系统的IO操作实现的,可以把Java中的BIO、NIO和AIO理解为Java语言对操作系统的各种IO模型的封装,程序员在使用这些IO相关的API时,不需要关心操作系统层面的知识,也不需要根据不同的操作系统编写不同的代码,只需要使用Java的IO相关的API即可。

一些概念的区分:

  • 同步:同步是指发起一个调用后,被调用者未处理完请求之前,调用不返回;
  • 异步:异步是指发起一个调用后,被调用者立刻返回一个结果表明自己已经收到请求,但是调用者不会返回真正的结果,此时调用者可以发起其他请求,被调用者通过回调机制等通知调用者真正的结果。

同步和异步最大的区别在于:同步是调用方需要主动地向被调用方获取信息,异步是指被调用方通过回调等机制通知调用方信息。

  • 阻塞:调用方线程在等待结果的过程中,线程被挂起,不能处理其他事情,直到结果返回;
  • 非阻塞:调用方线程在等待结果的过程中,线程没有被挂起,调用方线程可以去做其他事情。

阻塞和非阻塞最大的区别在于:阻塞是调用方线程在调用过程中不能做其他事情,而非阻塞是调用方线程在调用过程中可以做其他事情。
同步/异步强调的是消息如何返回给调用方,阻塞/非阻塞强调的是调用方线程在请求过程中是否处于挂起状态。
举例:

  • 同步阻塞:去医院挂号,医院的铃坏了,你担心错过叫号什么事情也不做就在那等(阻塞),眼睛一直盯着叫号屏幕看是否轮到自己就诊了(同步);
  • 同步非阻塞:去医院挂号,医院的铃坏了,你看人多就开始玩手机(非阻塞),同时眼睛时不时扫一下叫号屏幕看是否轮到自己了(同步);
  • 异步阻塞:去医院挂号,医院的铃是好的,你担心错过叫号什么事情也不做就在那等(阻塞),同时叫号铃会定时进行广播提示(异步);
  • 异步非阻塞:去医院挂号,医院的铃是好的,你此时一会玩会手机,一会看会电视剧(非阻塞),同时叫号铃会定时广播提示把排队信息传递给你(异步)。

    2、BIO

    2.1 BIO的运行模型

    下篇文章中介绍的Java传统IO,比如InputStream/OutPutStream、Writer/Reader都是BIO,同步阻塞式IO。BIO的运行模式如下:
  1. 当一个http请求过来时,服务器立马调用serverSocket.accept()接收一个socket,然后服务器从线程池中取一个线程并且将这个socket传给这个线程处理;
  2. 这个线程会调用serverSocket.read()读取socket中携带的数据流,然后将读取的数据流根据http协议解析成httprequest对象,然后将这个对象交给serverlet处理;
  3. 当servlet处理完毕之后,该线程将处理后返回的数据调用serverSocket.write()写回客户端。

阻塞在这个过程体现在读写两个方面:

  1. 一般网络IO操作都比较慢,比如线程在读取数据时,因为IO操作比较慢,所以线程要等到数据包在内核空间准备就绪后再发起读请求,数据在内核空间中准备期间,线程一直处于阻塞状态;
  2. 当线程向外写数据时,因为IO处理比较慢,因此要等待IO把数据都输出完毕并且关闭socket后才释放这个线程,在此期间线程一直处于阻塞状态。

    2.2 BIO模型存在的问题

    在BIO模型中,一个socket连接对应线程池中的一个线程,当有多个socket连接过来时,BIO需要使用多线程处理,由于socket.accept()、socket.read()、socket.write()三个主要函数都是同步阻塞的,当一个连接在处理I/O的时候,这个socket连接对应的线程是阻塞状态,这样线程的利用率并不高。
    BIO模型最大的问题也正是严重依赖于线程,但是线程是是很”贵”的资源,主要表现在:

  3. 线程的创建和销毁成本很高,在Linux这样的操作系统中,线程本质上就是一个进程。创建和销毁都是重量级的系统函数;

  4. 线程本身占用较大内存,像Java的线程栈,一般至少分配512K~1M的空间,如果系统中的线程数过千,恐怕整个JVM的内存都会被吃掉一半;
  5. 线程的切换成本是很高的。操作系统发生线程切换的时候,需要保留线程的上下文,然后执行系统调用。如果线程数过高,可能执行线程切换的时间甚至会大于线程执行的时间,这时候带来的表现往往是系统load偏高、CPU sy使用率特别高(超过20%以上),导致系统几乎陷入不可用的状态;
  6. 容易造成锯齿状的系统负载。因为系统负载是用活动线程数或CPU核心数,一旦线程数量高但外部网络环境不是很稳定,就很容易造成大量请求的结果同时返回,激活大量阻塞线程从而使系统负载压力过大。

BIO模型的痛点是过于依赖线程,当请求连接很多时,线程池中会存在上千线程,线程过多会带来上述四个问题,因此如何提高单个线程的利用率,让一个线程能处理多个socket链接是一个改进方向,NIO正是为了解决这个问题提出。

3、NIO

NIO是一种同步非阻塞模型,确切地讲Java的NIO模型对应的是操作系统的IO多路复用模型。Java 1.4中引入了NIO框架,对应java.nio包,提供了Channel、Buffer、Selector等抽象概念和对应的实现类。NIO模型中,在IO操作尚未就绪时,不阻塞读线程或者写线程,而是让这个线程去处理已经就绪好的IO操作,这样一个线程可以处理对应多个socket,提高了线程利用率。BIO中一个线程对应一个socket,NIO中一个线程可以对应多个socket。

3.1 NIO中的三个核心组件

3.1.1 Buffer(缓冲区)

传统的BIO面向流,而NIO面向缓冲区。在面向流的I/O中,可以将数据直接写入或者将数据直接读到 Stream 对象中,虽然 Stream 中也有 Buffer 开头的扩展类,但只是流的包装类,本质上还是流的处理。
Buffer是NIO类库的一个类,在NIO厍中,所有数据都是用缓冲区处理的。在读取数据时,它是直接读到缓冲区中的;在写入数据时,写入到缓冲区中。任何时候访问NIO中的数据,都是通过缓冲区进行操作。

3.1.2 Channel(通道)

NIO 通过Channel(通道) 进行读写。通道是双向的,可读也可写,而流的读写是单向的。无论读写,Channel只能和Buffer交互。因为 Buffer,通道可以异步地读写。
通常来说NIO中的所有IO都是从Channel开始的,如下:

  1. 从Channel中进行数据读取:创建一个Buffer,然后请求Channel读取数据;
  2. 从Channel进行数据写入:创建一个Buffer,填充数据,并要求Channel写入数据。

数据读取和写入操作如下图:
Java IO模型 - 图1

3.1.3 Selectors(选择器)

Selectors的概念是NIO中有而BIO没有的概念。Selectors对应IO多路复用模型中的select函数,一个socket对应一个Channel,一个Channel对应一个Buffer,一个selectors对应多个Channel,由一个线程轮询地遍历selectors,如下图所示:
NIO.png

3.2 NIO的运行模型

  1. 当一个http请求过来时,服务器立马将接收到的socket交给一个Channel,由这个Channel保持和客户端的连接状态;
  2. 随后这个Channel会向selectors注册一个连接事件,但此时不会调用线程让线程操作IO流,当Channel将所有的数据都写入到自己的Buffer中后,会向selectors注册一个读事件;
  3. 服务端的一个线程遍历selectors时会发现有一个读事件,此时才会调用线程池中的线程,将Channel传给这个线程,让线程去读取Buffer中已准备好的数据,并解析数据调用servlet处理数据;
  4. 等这个线程将数据返回时,会将返回的数据写入到这个Channel对应的Buffer中,然后向selectors注册一个写事件,此时线程无需等待数据全部输出后再释放,可以直接释放线程;
  5. 服务端的一个线程遍历selectors时发现这个写事件,此时会将Channel中的Buffer中的数据输出给客户端。

    3.3 NIO和IO的区别

  6. IO相关的类在java.io包下;NIO是Java 1.4才引入的,在java.nio包下;

  7. IO是对应BIO,是同步阻塞模型;NIO是同步非阻塞模型,从实现上讲是多路IO复用模型;
  8. IO是面向流的;NIO是面向缓冲区Buffer的,且NIO引入了Buffer、Channel和Selectors的概念;
  9. IO中一个线程对应一个socket;NIO中一个线程对应多个socket,NIO中线程的利用率更高,当并发数很高时NIO不会像IO起过多的线程,避免给CPU带来过大的开销以及占用过多的内存资源;
  10. IO中的流是单向的,分为输入流和输出流;NIO中的Channel是双向的,没有针对输入和输出做区分。

    4、AIO

    AIO 也就是 NIO 2。在 Java 7 中引入了 NIO 的改进版 NIO 2,它是异步非阻塞的IO模型。Netty虽然也尝试使用AIO,但后来放弃了,目前Netty还是以NIO为IO模型。

    参考

    常见的 IO 模型有哪些?Java 中的 BIO、NIO、AIO 有啥区别?
    BIO,NIO,AIO 总结
    JavaIO四大模型:NIO(IO多路复用)
    同步阻塞 同步非阻塞 异步阻塞 异步非阻塞
    Java中三种IO模式Bio,Nio,Aio 以及 Tomcat中的 Bio, Nio,Apr模式
    Java NIO浅析