最近正好不知道写什么,有粉丝朋友私信说对IO这块迷迷糊糊的,面试也说不好,所以决定做一系列的IO内容,这是第一篇,先来输出一波理论,后边再根据不同的IO模型使用不同的API实现一下,看看区别,再说一说常用的中间件的IO是怎么实现的,本章主要包含:


(资料图片仅供参考)

什么是IO,只是文件的读和写吗?IO在计算机中都是怎么实现的,原理是什么Java作为一个跨平台语言,提供了哪些IO模型屏蔽对底层系统细节

讲道理这篇文章写完自己也是颇有收获,建议收藏反复观看,不错的话记得【三连】哦

IO是什么

计算机的IO可以浅显的理解为输入【Input】和输出【Output】,通过IO描述数据的流动传输,在Java技术栈中IO更是基础,如硬盘上的文件读写需要用到磁盘IO,通过Tomcat处理网络请求需要设计网络IO,真是由于种类繁多的IO,很多小伙伴迷迷糊糊的,刚接触Java时我也如此,其实IO并不仅仅是文件的读写,Socket【通信】也存在IO操作

无论是Socket的读写还是文件的读写,在Java层面的应用开发或者是系统底层开发,都属于输入input和输出output的处理,简称为IO读写。在原理上和处理流程上,都是一致的。区别在于参数的不同。

用户程序对文件进行IO的读写,基本上会用到read&write两大系统调用。可能不同操作系统【如windows和Linux系统具体的处理不同】,名称和实现方式不同,但是功能是一样的。

IO实现原理

无论是读还是写都需要调用底层系统的功能,read读取数据,并不是把数据直接从物理设备读到内存。write写入数据,也不是直接把数据写入到物理设备。

read系统调用:是把数据从内核缓冲区复制到进程缓冲区;

write系统调用:是把数据从进程缓冲区复制到内核缓冲区。

这个两个系统调用,都不负责数据在内核缓冲区和磁盘之间的交换。真正与硬件交互的是由操作系统kernel内核完成,需要通过操作系统实现数据的读写

内核缓冲与进程缓冲区

缓冲区的目的,是为了减少频繁的系统IO调用。系统调用需要保存之前的进程数据和状态等信息【比如读文件时需要知道已经读了多少数据啦,不要重复读取】,而结束调用之后回来还需要恢复之前的信息,为了减少这种损耗时间、也损耗性能的系统调用,于是出现了缓冲区。

有了缓冲区,操作系统使用read函数把数据从内核缓冲区复制到进程缓冲区,write把数据从进程缓冲区复制到内核缓冲区中。等待缓冲区达到一定数量的时候,再进行IO的调用,提升性能。至于什么时候读取和存储则由内核来决定,用户程序不需要关心。

在linux系统中,系统内核也有个缓冲区叫做内核缓冲区。每个进程有自己独立的缓冲区,叫做进程缓冲区。

所以,用户程序的IO读写程序,大多数情况下,并没有进行实际的IO操作,而是在读写自己的进程缓冲区。

就好比村民需要到村中心水房挑水倒入自家的水缸中满足正常生活用水需求,如果拿水瓢得一直跑来跑去,效率极低,搞个水桶当做缓冲区,每次用水瓢把水桶盛满【缓冲区到达极限】,再拎着水桶回家将水倒入水缸【系统内核】

BIO【Blocking IO】

在linux系统的Java进程中,默认情况下所有的socket都是blocking IO。是一种阻塞式 I/O 模型,应用程序从系统开始执行IO,一直到系统结束调用返回,这段时间是阻塞的。返回成功后,应用进程开始处理用户空间的缓存数据

发起一个blocking socket的read读操作系统调用,流程大概是这样:

1、当用户线程调用read,内核(kernel)就开始了IO的第一个阶段:准备数据。很多时候,数据在一开始还没有到达(比如,还没有收到一个完整的Socket数据包),这个时候kernel就要等待足够的数据到来;

2、当kernel一直等到数据准备好了,它就会将数据从kernel内核缓冲区,拷贝到用户缓冲区(用户内存),然后kernel返回结果;

3、从开始IO读的read系统调用开始,用户线程就进入阻塞状态。一直到kernel返回结果后,用户线程才解除block的状态,重新运行起来;

所以,blocking IO的特点就是在内核进行IO执行的两个阶段,用户线程都被阻塞。

BIO的优点:程序简单,在阻塞等待数据期间,用户线程挂起。用户线程基本不会占用 CPU 资源。

BIO的缺点:一般情况下,会为每个连接配套一条独立线程,或者说一条线程维护一个连接成功的IO流的读写。在并发量小的情况下没有什么问题。但是,当在高并发的场景下,需要大量的线程来维护大量的网络连接,内存、线程切换开销会非常巨大。因此,BIO模型在高并发场景下基本不可用

NIO【non-blocking IO】

在linux系统下,可以通过设置socket使其变为non-blocking。NIO模型中应用程序一旦开始IO调用,会出现以下两种情况:

1、在内核缓冲区没有数据的情况下,系统调用会立即返回,返回一个调用失败的信息。

2、在内核缓冲区有数据的情况下,是阻塞的,直到数据从内核缓冲复制到用户进程缓冲。复制完成后,系统调用返回成功,应用进程开始处理用户空间的缓存数据。

发起一个non-blocking socket的read读操作系统调用,流程是这个样子:

1、在内核数据没有准备好的阶段,用户线程发起IO请求时,立即返回。用户线程需要不断地发起IO系统调用;

2、内核数据到达后,用户线程发起系统调用,用户线程阻塞。内核开始复制数据。它就会将数据从kernel内核缓冲区,拷贝到用户缓冲区(用户内存),然后kernel返回结果;

3、用户线程才解除block的状态,重新运行起来。经过多次的尝试,用户线程终于真正读取到数据,继续执行。

NIO的特点:应用程序的线程需要不断的进行 I/O 系统调用,轮询判断数据是否已经准备好,如果没有准备好,继续轮询,直到完成系统调用为止。

NIO的优点:每次发起的 IO 系统调用,在内核的等待数据过程中可以立即返回。用户线程不会阻塞,实时性较好。

NIO的缺点:需要不断的重复发起IO系统调用,这种不断的轮询,将会不断地询问内核,这将占用大量的 CPU 时间,系统资源利用率较低。

总之,这种NIO模型在高并发场景下也是不可用的。一般 Web 服务器不使用这种 IO 模型。而是在其他IO模型中使用非阻塞IO这一特性。Java的实际开发中,也不会涉及这种IO模型。

注意:Java NIO【New IO】 不是IO模型中的NIO模型,而是另外的一种模型,叫做IO多路复用模型【IO multiplexing 】。

IO多路复用模型【I/O multiplexing】

如何避免同步非阻塞NIO模型中轮询等待的问题呢?这就是IO多路复用模型。也就是Java NIO的实现模型

IO多路复用模型,就是一个进程可以监视多个文件描述符,一旦某个描述符就绪【一般是内核缓冲区可读/可写】,内核kernel能够通知程序进行相应的IO系统调用。

目前支持IO多路复用的系统调用,有 select,epoll等等。select系统调用,是目前几乎在所有的操作系统上都有支持,具有良好跨平台特性。epoll是在linux 2.6内核中提出的,是select系统调用的linux增强版本。

IO多路复用模型的基本原理就是select/epoll系统调用,单个线程不断的轮询select/epoll系统调用所负责的成百上千的socket连接,当某个或者某些socket网络连接有数据到达了,就返回这些可以读写的连接。因此,好处就是通过一次select/epoll系统调用,就查询到到可以读写的一个甚至是成百上千的网络连接。

发起一个多路复用IO的的read读操作系统调用,流程是这个样子:

在这种模式中,首先不是进行read系统调动,而是进行select/epoll系统调用。当然,这里有一个前提,需要将目标网络连接,提前注册到select/epoll的可查询socket列表中。然后,才可以开启整个的IO多路复用模型的读流程。

1、进行select/epoll系统调用,查询可以读的连接。kernel会查询所有select的可查询socket列表,当任何一个socket中的数据准备好了,select就会返回。当用户进程调用了select,那么整个线程会被阻塞掉;

2、用户线程获得了目标连接后,发起read系统调用,用户线程阻塞,内核开始复制数据,它就会将数据从kernel内核缓冲区,拷贝到用户缓冲区【用户内存】,然后kernel返回结果;

3、用户线程解除阻塞状态,用户线程终于真正读取到数据,继续执行。

多路复用IO的特点:IO多路复用模型,建立在操作系统kernel内核能够提供的多路分离系统调用select/epoll基础之上的。多路复用IO需要用到两个系统调用【system call】, 一个select/epoll查询调用,一个是IO的读取调用。

和NIO模型相似,多路复用IO需要轮询。负责select/epoll查询调用的线程,需要不断的进行select/epoll轮询,查找出可以进行IO操作的连接。

另外,多路复用IO模型与前面的NIO模型,是有关系的。对于每一个可以查询的socket,一般都设置成为non-blocking模型。只是这一点,对于用户程序是透明的。

多路复用IO的优点:用select/epoll的优势在于,它可以同时处理成千上万个连接。与一条线程维护一个连接相比,I/O多路复用技术的最大优势是:系统不必创建线程,也不必维护这些线程,从而大大减小了系统的开销。

Java的NIO技术,使用的就是IO多路复用模型。在linux系统上,使用的是epoll系统调用。

多路复用IO的缺点:本质上,select/epoll系统调用,属于同步IO,也是阻塞IO。都需要在读写事件就绪后,自己负责进行读写,也就是说这个读写过程是阻塞的。

如何充分的解除线程的阻塞呢?那就是异步IO模型。

AIO【Asynchronous IO】

如何进一步提升效率,解除最后一点阻塞呢?这就是异步IO模型,全称asynchronous I/O,简称为AIO。

AIO的基本流程是:用户线程通过系统调用,告知kernel内核启动某个IO操作,用户线程返回。kernel内核在整个IO操作【包括数据准备、数据复制】完成后,通知用户程序,用户执行后续的业务操作。

kernel的数据准备是将数据从网络物理设备【网卡】读取到内核缓冲区;kernel的数据复制是将数据从内核缓冲区拷贝到用户程序空间的缓冲区。

1、当用户线程调用了read系统调用,立刻就可以开始去做其它的事,用户线程不阻塞;

2、kernel内核就开始了IO的第一个阶段:准备数据。当kernel一直等到数据准备好了,它就会将数据从kernel内核缓冲区,拷贝到用户缓冲区;

3、kernel会给用户线程发送一个信号【事件】,或者回调用户线程注册的回调接口,告诉用户线程read操作完成了;

4、用户线程读取用户缓冲区的数据,完成后续的业务操作。

异步IO模型的特点:在内核kernel的等待数据和复制数据的两个阶段,用户线程都不是阻塞的。用户线程需要接受kernel的IO操作完成的事件,或者说注册IO操作完成的回调函数,到操作系统的内核。所以说,异步IO有的时候,也叫做信号驱动 IO 。

异步IO模型缺点:需要完成事件的注册与传递,这里边需要底层操作系统提供大量的支持,去做大量的工作。

目前来说, Windows 系统下通过 IOCP 实现了真正的异步 I/O。但是,就目前的业界形式来说,Windows 系统很少作为百万级以上或者说高并发应用的服务器操作系统来使用。

而在 Linux 系统下,异步IO模型在2.6版本才引入,目前并不完善。所以这也是在 Linux 下,实现高并发网络编程时都是以 IO 复用模型模式为主。

这里介绍完了IO的几种基础模型,接下来解释一下同步异步,阻塞非阻塞的概念之后进而说明Java中的IO实现

同步与异步同步【synchronous】:同步是一种可靠的有序运行机制,任务需要一个一个执行。异步【asynchronous】:异步就需要等待上一个任务执行完毕,可以依靠事件、回调等机制执行

同步和异步的最大区别在于:异步不需要等待处理结果,被调用者会通过回调等机制来通知调用者其返回结果。

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

通过生活中烧水的例子解释一下同步阻塞、同步非阻塞、异步阻塞、异步非阻塞代表什么?

烧水需要一个人【张三】和一个水壶

同步阻塞:张三用比较落后的水壶在炉子上烧水,因为安全起见,烧水过程中张三等待不做其他任何事情【阻塞】,水开之后张三为了不浪费燃料,手动将火关掉【同步】;

同步非阻塞:张三发现水开的时候有滋滋滋声,点火之后就去看电视了【非阻塞】,听到水开之后,回来将火关掉【同步】;

此处发现:同步就是需要手动进行下一步操作,阻塞与非阻塞的区别就在于就是在执行任务时是否需要等待完成

异步阻塞:张三使用了先进的电热水壶,水开之后会自动关闭【异步】,但是这哥们不放心科技产品,非要等着水烧好水壶自己断电才行【阻塞】

异步非阻塞:时间久了之后张三发现产品没有问题,每次都可以正常断电,他就把电通上之后就去忙其他事情【非阻塞】,水烧开之后,电水壶自动跳电【异步】

此处发现:异步是根据水壶根据水是否烧开的事件来触发断电这个流程,而非手动调用断电

Java中的BIO,NIO,AIO

Java 中的 BIO、NIO和 AIO 理解为是 Java 语言对上述所介绍的操作系统的各种 IO 模型的封装。在使用这些 API 的时候,不需要关心操作系统层面的知识,也不需要根据不同操作系统编写不同的代码。只需要使用Java的API就可以了。我们首先了解一下Java中各种IO的特点和演进之路

IO

I/O是Input和Output的简写,Input对应的是输入,Output对应的是输出,可以理解为依靠什么模式来完成数据的发送和接收,很大程度上决定了数据传输的性能

对于计算机来说,键盘打字属于将字符输入到计算机内,点击鼠标将信号输入到计算机内,这属于输入。显示器是将文字,图像显示出来,音响将声音播放出来这属于输出。

Java具体实现的IO有三种:BIO、NIO、AIO

BIO

BIO是Java的第一代IO模型,Blocking Input Output的缩写,是一种同步阻塞型IO,服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理,如果这个连接不作任何事情会造成不必要的线程开销

也就是一次输入或者输出操作必须等待它完成之后才会继续向下运行其他的操作,如果一个输入或者输出操作需要很长的时间,比如要上传一个5G的文件,需要等待上传完才能继续执行其他操作,这会造成不必要的时间浪费

NIO

Java 1.4中引入了NIO,也就是Java的第二代IO模型,有人称之为Non-blocking IO,也有的人称为New IO,它是一种多路复用的同步非阻塞IO模型,对应上边的第三种IO模型

多路复用:指的就是用同一个线程处理大量连接

多路:指的就是大量连接

复用:指的就是复用线程,使用一个线程来进行处理

服务器实现模式为一个线程处理多个请求(连接),即客户端发送的连接请求会被注册到多路复用器上,多路复用器轮询到有 I/O 请求就会进行处理。

AIO

Java 7中引入AIO,是Asynchronous Input Output的缩写,可以称之为NIO第二代,是一种异步非阻塞IO模型,异步IO模型是基于事件和回调机制实现的,当应用发起调用请求之后会直接返回不会阻塞在那里,当后台进行数据处理完成后,操作系统便会通知对应的线程来进行后续的数据处理。

从效率上来看,AIO 无疑是最高的,然而,美中不足的是目前作为广大服务器使用的系统 linux 对 AIO 的支持还不完善,导致我们还不能愉快的使用 AIO 这项技术,Netty实际也是使用过AIO技术,但是实际并没有带来很大的性能提升,目前还是基于Java NIO实现。

推荐内容