在研究java的IO模型前有必要先对操作系统的IO模型有个认识。
操作系统层面的IO模型
在Linux(UNIX)操作系统中,共有五种IO模型,分别是:
阻塞IO模型
- 阻塞 I/O 是最简单的 I/O 模型,一般表现为进程或线程等待某个条件,如果条件不满足,则一直等下去。条件满足,则进行下一步操作。
非阻塞IO模型
- 应用进程与内核交互,目的未达到之前,不再一味的等着,而是直接返回。然后通过轮询的方式,不停的去问内核数据准备有没有准备好。如果某一次轮询发现数据已经准备好了,那就把数据拷贝到用户空间中。
IO复用模型
- 多个进程的IO可以注册到同一个管道上,这个管道会统一和内核进行交互。当管道中的某一个请求需要的数据准备好之后,进程再把对应的数据拷贝到用户空间中。
信号驱动IO模型
- 应用进程在读取文件时通知内核,如果某个 socket 的某个事件发生时,请向我发一个信号。在收到信号后,信号对应的处理函数会进行后续处理。
以上模型都是同步的,原因是因为,无论以上那种模型,真正的数据拷贝过程,都是同步进行的。信号驱动IO模型只能说 数据准备阶段是异步,数据拷贝操作还是同步的。只有系统将数据已经全都从内核空间拷贝到用户空间然后再发信号通知线程已经完成。
- 异步IO模型。
- 应用进程把IO请求传给内核后,完全由内核去操作文件拷贝。内核完成相关操作后,会发信号告诉应用进程本次IO已经完成。
java中三种IO模型
在Java中,主要有三种IO模型:
- 阻塞IO(BIO/Blocking I/O)
- 同步阻塞I/O模式,数据的读取写入必须阻塞在一个线程内等待其完成。
- 非阻塞IO(NIO)
- 一种叫非阻塞IO(Non-blocking I/O),另一种也叫新的IO(New I/O),其实是同一个概念。它是一种同步非阻塞的I/O模型,也是I/O多路复用的基础。
- NIO是一种基于通道和缓冲区的I/O方式,它可以使用Native函数库直接分配堆外内存(区别于JVM的运行时数据区),然后通过一个存储在Java堆里面的DirectByteBuffer对象作为这块内存的直接引用进行操作。这样能在一些场景显著提高性能,因为避免了在Java堆和Native堆中来回复制数据。
- 异步IO(AIO/Asynchronous I/O)
- AIO 也就是 NIO 2。在 Java 7 中引入了 NIO 的改进版 NIO 2,它是异步非阻塞的IO模型。异步 IO 是基于事件和回调机制实现的,也就是应用操作之后会直接返回,不会堵塞在那里,当后台处理完成,操作系统会通知相应的线程进行后续的操作。(目前来说使用还不广泛,netty使用的是NIO)
NIO与BIO的区别
1)BIO以流的方式处理数据,而NIO以块的方式处理数据,块I/O的效率比流I/O高很多;
2)BIO是阻塞的,NIO是非阻塞的;
3)BIO基于字节流和字符流进行操作,而NIO基于Channel(通道)和Buffer(缓冲区)进行操作,数据总是从通道读取到缓冲区中,或者从缓冲区写入到通道中。Selector(选择器)用于监听多个通道的时间(比如:连接请求,数据到达等),因此使用单个线程就可以监听多个客户端通道;
4)BIO是单向的,如:InputStream, OutputStream;而NIO是双向的,既可以用来进行读操作,又可以用来进行写操作。
NIO主要组件介绍
Buffer
Buffer(缓冲区)是一个用于存储特定基本类型数据的容器。除了boolean外,其余每种基本类型都有一个对应的buffer类。
Buffer类的子类(基础类型中除了boolean外都有对应的buffer)有:
- ByteBuffer
- CharBuffer
- DoubleBuffer
- FloatBuffer
- IntBuffer
- LongBuffer
- ShortBuffer
Buffer常用方法
- 建立缓冲区分配容量:allocate(capacity)
- 装载数据:put() 及其重载方法
- 改变缓冲区的读写模式:flip()
- 获取数据:get() 及其重载方法
- 标记:mark()/reset()
- 判断:remaining():返回当前位置与限制之间的元素数;
hasRemaining():判断当前位置与限制之间是否还有元素存在;
isReadOnly():判断此缓冲区是否为只读缓冲区;
clear():清空缓冲区,只是将缓冲区的三个属性恢复到初始状态,其中的数据依然存在。
Channel
Channel(通道)表示到实体,如硬件设备、文件、网络套接字或可以执行一个或多个不同 I/O 操作(如读取或写入)的程序组件的开放的连接。
Channel接口的常用实现类有:
- FileChannel(对应文件IO)
- DatagramChannel(对应UDP)
- SocketChannel和ServerSocketChannel(对应TCP的客户端和服务器端)
Channel和IO中的Stream(流)是差不多一个等级的。
Selector
Selector(选择器)用于监听多个通道的事件(比如:连接打开,数据到达)。因此,单个的线程可以监听多个数据通道。即用选择器,借助单一线程,就可对数量庞大的活动I/O通道实施监控和维护。
IO多路复用
IO多路复用(IO Multiplexing) 是这么一种机制:程序注册一组socket文件描述符给操作系统,表示“我要监视这些fd是否有IO事件发生,有了就告诉程序处理”。
IO多路复用是要和NIO一起使用的。尽管在操作系统级别,NIO和IO多路复用是两个相对独立的事情。NIO仅仅是指IO API总是能立刻返回,不会被Blocking;而IO多路复用仅仅是操作系统提供的一种便利的通知机制。操作系统并不会强制这俩必须得一起用——你可以用NIO,但不用IO多路复用;也可以只用IO多路复用 + BIO,这时效果还是当前线程被卡住。但是,IO多路复用和NIO是要配合一起使用才有实际意义。因此,在使用IO多路复用之前,请总是先把fd设为O_NONBLOCK
。
对IO多路复用,还存在一些常见的误解,比如:
❌IO多路复用是指多个数据流共享同一个Socket。其实IO多路复用说的是多个Socket,只不过操作系统是一起监听他们的事件而已。
多个数据流共享同一个TCP连接的场景的确是有,比如Http2 Multiplexing就是指Http2通讯中中多个逻辑的数据流共享同一个TCP连接。但这与IO多路复用是完全不同的问题。
❌IO多路复用是NIO,所以总是不Block的。其实IO多路复用的关键API调用(
select
,poll
,epoll_wait
)总是Block的。❌IO多路复用和NIO一起减少了IO。实际上,IO本身(网络数据的收发)无论用不用IO多路复用和NIO,都没有变化。请求的数据该是多少还是多少;网络上该传输多少数据还是多少数据。IO多路复用和NIO一起仅仅是解决了调度的问题,避免CPU在这个过程中的浪费,使系统的瓶颈更容易触达到网络带宽,而非CPU或者内存。要提高IO吞吐,还是提高硬件的容量(例如,用支持更大带宽的网线、网卡和交换机)和依靠并发传输(例如HDFS的数据多副本并发传输)。