Java基础篇--IO

本文详细介绍了Java中的IO模型,包括传统的BIO、NIO以及AIO,并讲解了网络IO模型如阻塞IO、非阻塞IO、IO多路复用(select、poll、epoll)和信号驱动IO、异步IO。此外,还提到了Java的reactor和proactor设计模式以及NIO的selector、channel和buffer组件。了解这些知识对Java面试和理解网络通信至关重要。
摘要由CSDN通过智能技术生成

在之前的文章中,已经发布了常见的面试题,这里我花了点时间整理了一下对应的解答,由于个人能力有限,不一定完全到位,如果有解答的不合理的地方还请指点,在此谢过。

本文主要描述的是java面试中出现频率较高的IO。看过jdk源码的同学可能知道,java的IO模型分为两种,传统的IO和NIO。传统的IO就是我们平时常用的BIO(Blocking IO),它的包名是java.io;而NIO(NoBlocking IO)的包名是java.nio。网络io其实是面试中常见的问题,掌握这个对我们的面试会有很大的帮助。下面我们就io的常见面试题进行解答说明。

 

能简单说明一下你了解的网络IO模型么?

在回答这个问题之前,先来看下操作系统在完成一次IO的过程需要经过哪些操作。对于操作系统而言,外部设备的操作都可以看作是对文件的操作,当我们读写一个文件的时候,系统内核会返回一个文件描述符(file descriptor),该文件描述符指向内核中的一个结构体,我们操作文件就是对该结构体进行操作。我们都知道,在Linux系统中,内存被分为内核态和用户态。Linux的内核态管理所有接入的硬件资源,并且会提供相关的函数供用户态调用。在了解了这些操作系统上的概念之后,我们以读操作来描述一下一个IO过程。当我们的程序在调用读操作的时候,操作系统会发起read指令,同时在内核态中创建一个文件描述符,等待把读的数据放入到文件描述符指定的结构体缓冲区后然后将缓冲区的数据读回到用户态的内存中,这样就完成了一次IO的操作。我们可以用下面的示意图来描述一下io的过程。

基本上一个io的过程需要经过上图的两个步骤:

  1. 等待数据准备,将硬件读取数据到指定的内核缓冲区中(过程2)
  2. 将缓冲区的内容读回到用户态的应用程序中(过程1)

当然了写的过程就是相反的,把我们的数据写到硬件设备中。有了上面对于的知识,下面我们解释几个io中名词。

阻塞和非阻塞:阻塞和非阻塞的区别在上述描绘的两个阶段中,如果第一阶段就阻塞在那里等待数据的话,我们称为阻塞,否则是非阻塞。

同步和异步:同步和异步的区别在于如果两个阶段都不需要应用程序阻塞等待的话,那么称为异步,否则称为同步。

可能看完上面的基本概念之后会有些模糊,那么现在我们引用一下Unix网络编程中描述的5种网络io模型。

阻塞io

阻塞io是应用程序在调用io操作时,应用程序会一直等待内核操作完成,直到把数据返回回来。其特点在于两个阶段都阻塞了

非阻塞io

非阻塞io是在第一阶段发送请求之后,会立即得到数据是否准备好了,然后应用程序不断发起请求询问数据准备的情况,直到数据准备完成。进入第二阶段阻塞式拷贝回用户态。其特点是:第一阶段不阻塞,不过需要用户程序不断轮询数据准备的情况

Io多路复用

Io多路复用基于操作系统提供了select的分离函数。用户在第一阶段将socket添加到select中,然后阻塞等待select系统调用返回,当数据到达时,socket被激活然后发起read操作。其特点是:对于单个socket来说和阻塞io基本类似,甚至还更差点,但其优势在于用户可以在一个线程中添加多个socket请求,换句话说用户可以在一个线程内请求和处理多个io请求的目的。我们知道io的操作中本身就是io耗时,而cpu使用率较低,如果我们将多个io的请求放到一个线程上的话,对cpu来说完全是没有任何问题的,这也是其优势的地方。

Io多路复用是最长问的一个io网络模型,也是现在使用最多的场景。其实现方式有select,poll和epoll,这三者的区别也是面试中的常问题目之一。

 

信号驱动io

信号驱动io是在第一步的时候,用户程序会向内核注册一个 信号处理函数,然后用户进程返回,当内核数据就绪时,向该进程发送一个信号,用户进程在信号处理函数中读取数据。其特点是:第一个过程是不阻塞的,第二个过程依然是阻塞的,读取数据的过程依然是用户来完成

异步io

异步io是用户程序发起一个aio_read的函数操作之后,该函数会告诉内核描述符,缓冲区的指针,缓冲区的大小等信息,然后会立即返回,而内核会将数据准备完成,并且将数据拷贝到用户态内存中,并且通知程序完成了操作。在Linux操作系统中,并没有实现真正意义上的异步io,因为异步io将很多事情都交给了内核去处理,其处理速度取决于用户需要操作的数据的大小,如果数据量很大,内核速度很慢,如果数据量很小,那io多路已经足够快了。而Windows系统的iocp实现方式是真正意义上实现了异步io。 其特点:真正的实现了异步

 

io多路复用的select、poll、epoll的区别是什么?

Io多路复用是现在高性能的组件的核心网络io模型,一般我们看一个中间件的话,基本上都会使用到io多路复用。那么下面我们看下io多路复用的三种调用方式:

Select:

int select(int maxfdp1,fd_set *readset,fd_set *writeset,fd_set *exceptset,const struct timeval *timeout);

typedef struct{

    long int fds_bits[32];//每一位可以代表一个文件描述符,所以是1024个

}fd_set;

 

参数值:

maxfdp1 待测试文件描述符数量+1。

*readset , *writeset ,  *exceptset 分别是内核测试读、写和异常条件的文件描述符集合。

*timeout 任何一个文件描述符准备就绪的超时时间。

返回值:已经就绪的文件描述符的数量

 

 

Select有几个不足之处:

  1. 每次调用select的时候,都会把fd_set的几个参数从用户态到内核态的拷贝
  2. 内核会线性遍历所有传入的文件描述符是否有准备就绪
  3. 为了限制内核的开销,fd_set的数量被限制在1024以内

 

Poll:

int poll(struct pollfd *fds, nfds_t nfds, int timeout);

typedef struct pollfd {

        int fd;                         // 文件描述符

        short events;                   // 文件描述符的监听事件

        short revents;                  // 文件描述符fd上正在发生的事件

} pollfd_t;

参数值:

*fds:文件描述符的链表存储结构

nfds:fds的总数量

Timeout:超时时间

 

Poll是select的改进版,基本原理和select类似,其改进的有点在于:

修改了之前fd_set的方式来描述文件描述符,而使用pollfd方式,之前fd_set是一个数组结构,而pollfd是一个链表的结构,从而没有1024的限值

 

Epoll:

int epoll_create(int size);//创建一个文件描述符为size的epoll

int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);//注册要监听的事件类型

int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);//等待事件就绪函数

Epoll是Linux操作系统出现的一种基于事件驱动的io模式,其原理还是io多路复用,不过其改进点在于以下几方面:

  1. epoll在内核里面使用事件表的方式记录了文件描述符相关信息(底层是基于mmap保存),这样就只需要拷贝一次用户态到内核态。
  2. Epoll底层使用红黑树的数据结构存储文件描述符的信息
  3. Epoll采用事件回调机制,注册fd的时候会添加关注的事件,一旦fd就绪就会主动通知内核,从而避免每次线性扫描

 

Epoll的两种触发方式有什么区别?

Epoll有水平触发(LT)和边沿触发(ET)两种方式。

水平触发:是默认的触发方式,当epoll_wait检测到某文件描述符事件就绪并通知应用程序时,应用程序可以选择不立即处理;下次调用epoll_wait时,会再次通知此事件。其核心在于只要是就绪态就会通知

边沿触发:当epoll_wait检测到某文件描述符事件就绪并通知应用程序时,应用程序必须立即处理该事件。如果不处理,下次调用epoll_wait时,不会再次通知此事件。其核心在于只有未就绪到就绪的上升沿会触发通知

从上面可以知道et可以减少通知的次数,但对应用程序要求高,必须立马处理,否则可能会丢消息。而lt则是通知的次数变多,但是不会丢消息,在绝大部分的情况下。我们都使用lt的模式

 

有了解过reactor和proactor模型么?

如果学过netty的同学,多多少少会知道reactor模型,毕竟netty整个的设计模式就是reactor模型的经典设计。下面我们就来聊聊这两种io设计模式。

Reactor模式同步io的一种常用设计方式,其关注的是事件就绪,基于事件驱动,特别适合处理大量io事件。一般reactor模型中有以下三种角色:

Reactor:监听和分配事件,将io事件分配handler。

Acceptor:处理客服端请求,并转发到reactor中进行处理。

Handler:阻塞的完成channel的读入和写出,完成主要的逻辑。

单线程的reactor模型如下几个组件构成:

然后再看下netty的reactor模型:

Bossgroup和workgroup是reactor组件,eventloop绑定的thread进行相应的handler事件处理。

Proactor模型是基于异步io实现的,又被称为主动器模式,其关注的是事件完成。一般也有以下几个角色:

Proactor initiator:创建Proactor和handler,并将两者通过异步处理器注册到内核中。

异步处理器:负责处理注册请求,并完成io操作,完成后通知Proactor。

Proactor:Proactor负责回调不同的handler进行业务处理。

Handler:业务处理器,处理业务逻辑。

其主要的实现模型图如下:

其实reactor和Proactor的核心区别在于同步io和异步io的设计。

 

说说你对java整个io包的理解,java在设计io的时候使用了哪两种设计模式?

在Java中,将不同的终端的输入输出源抽象为流的概念,通过这种方式可以将不同终端的数据进行交互。Java将传统的流类型都放在java.io包中,一般我们将IO以以下三种方式进行分类。

  1. 按照流的方向:输入流和输出流

输入输出流是针对程序运行的内存而言的,从内存输出到其他介质上称为输出流,从其他介质输入到程序内存中称为输入流。一般以inputstream/reader结尾的称为输入流,outputstream/writer结尾的称为输出流。

  1. 按照操作单元分:字节流和字符流

   字节流是针对一个字节一个字节进行操作的,字符流最小的操作单元是一个字符。字节流以inputstream/outputstream结尾,字符流以writer/reader结尾。字符流和字节流的转换使用适配器模式进行

  1. 按照是否可以直接连接一个介质来划分:节点流和处理流

节点流是指可以直接连接在一个介质上进行输入输出的,而处理流是将节点流进行包装而具有新的功能,节点流也被称为低级流,处理流因为有了新的功能特性(当然了也包含了原来的特性)被称为高级流。这里jdk使用装饰器模式进行

整个io包中的可以如下分类:

 

NIO的三大组件了解么?说说channel和stream的区别?

在nio包中,围绕着三个核心的组件进行,分别是selector,channel和buffer。selector是事件选择器,通过监听不同的响应事件将选择不同的channel中,所以selector和channel是一对多的关系。Channel和buffer是一起的,我们通过channel将数据写入buffer,也需要通过channel将数据从buffer中读出,channel和buffer是一对一的关系。下面我们通过一个模型图表示它们三者之间的联系。

 

Channel和stream有点类似,但是又有实际的区别:

  1. channel是双向的,stream是单向的
  2. Channel可以是异步的读写
  3. Stream是io里面的概念,是一个流的概念,而channel是nio里面的概念,是一个通道的概念。

Java的io是平时经常使用的,也是面试中长考察的知识点,尤其是网络io。有时间和兴趣的同学可以掌握一下netty的框架,对后续了解好多架构的底层网络通信模块都有帮助。

本文的内容就这么多,如果你觉得对你的学习和面试有些帮助,帮忙点个赞或者转发一下哈,谢谢。

 

想要了解更多java内容(包含大厂面试题和题解)可以关注公众号,也可以在公众号留言,帮忙内推阿里、腾讯等互联网大厂哈

                                 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值