高频面试题-Java IO【java面试】

前言

2023-4-7 21:36:31

以下内容源自
牛客
仅供学习交流使用

推荐

牛客职导秋招冲刺—Java

高频面试题-Java IO

参考书目

在这里插入图片描述

- / 背景

操作系统的核心资源(CPU、内存、网络、IO、驱动)均由内核进行管理,为了避免用户直接操作内核,保证内核的安全,操作系统将内存寻址空间划分为两部分:内核空间、用户空间。

针对I/O操作,以读取为例来说,数据需要由磁盘拷贝到内核缓冲区,再由内核缓冲区拷贝到用户缓冲区。这两次拷贝均需要一定的时间,所以操作系统支持不同的策略来实现这种拷贝,以提高性能。

在这里插入图片描述

一.Unix I/O模型

- / 简介

Linux的内核将所有外部设备都看做一个文件来操作,对一个文件的读写操作会调用内核提供的系统命令,返回一个file descriptor(fd,文件描述符)。而对一个socket的读写也会有相应的描述符,称之为socket fd(socket描述符)。描述符就是一个数字,它指向内核中的一个结构体(文件路径,数据区等一些属性)。

根据UNIX网络编程对I/O模型的分类,UNIX提供了5种I/O模型:阻塞I/O模型、非阻塞式I/O模型、I/O复用模型、信号驱动I/O模型、异步I/O模型。

感觉有点类似
联系:第六章 输入输出系统【操作系统】
6.4.3 对I/O设备的控制方式
在这里插入图片描述

01 / 阻塞I/O模型

最常用的I/O模型就是阻塞I/O模型,缺省情况下,所有文件操作都是阻塞的。我们以套接字接口为例讲解此模型:在进程空间中调用recvfrom,其系统调用直到数据包到达且被复制到应用进程的缓冲区中或者发生错误时才返回,在此期间会一直等待,进程再从调用recvfrom开始到它返回的整段时间内都是被阻塞的,因此被称为阻塞I/O模型。
在这里插入图片描述

02 / 非阻塞I/O模型

recvfrom从应用层到达内核的时候,如果该缓冲区没有数据的话,就直接返回一个EWOULDBLOCK错误,一般都对非阻塞I/O模型进行轮询检查这个状态,看内核是不是有数据到来。

在这里插入图片描述

03 / I/O复用模型

Linux提供select/poll,进程通过将一个或多个fd传递给select或poll系统调用,阻塞在select操作上,这样select/poll可以帮我们侦测多个fd是否处于就绪状态。select/poll是顺序扫描fd是否就绪,而且支持的fd数量有限,因此它的使用受到了一些制约。

Linux还提供了一个epoll系统调用,epoll使用基于事件驱动方式代替顺序扫描,因此性能更高。当有fd就绪时,立即回调函数callback。I/O复用模型的优势是,我们可以等待多个描述符就绪。

在这里插入图片描述

04 / 信号驱动I/O模型

首先开启套接口信号驱动I/O功能,并通过系统调用sigaction执行一个信号处理函数(此系统调用立即返回,进程继续工作,它是非阻塞的)。当数据准备就绪时,就为该进程生成一个SIGIO信号,通过信号回调通知应用程序调用recvfrom来读取数据,并通知主循环函数处理数据。

在这里插入图片描述

05 / 异步I/O模型

告知内核启动某个操作,并让内核在整个操作完成后(包括将数据从内核复制到用户自己的缓冲区)通知我们。这种模型与信号驱动模型的主要区别是:信号驱动I/O由内核通知我们何时开始一个I/O操作;异步I/O模型由内核通知我们I/O操作何时已经完成。

在这里插入图片描述

06 / 五种I/O模型的比较

在这里插入图片描述

I/O复用的优势:一个线程监视多个文件描述符
只有第5种是异步的

07 / I/O多路复用技术

I/O多路复用技术通过把多个I/O的阻塞复用到同一个select的阻塞上,从而使得系统在单线程的情况下可以同时处理多个客户端请求。与传统的多线程/多进程模型比,I/O多路复用的最大优势是系统开销小,系统不需要创建新的额外的进程或者线程,也不需要维护这些进程和线程的运行,降低了系统的维护工作量,节省了系统资源。

目前支持I/O多路复用的系统调用有select、pselect、poll、epoll,在Linux网络编程过程中,很长一段时间都使用select做轮询和网络事件通知,然而select的一些固有缺陷导致了它的应用受到了很大的限制,最终Linux不得不在新的内核版本中寻找select的替代方案,最终选择了epoll。

为了克服select的缺点,epoll做了很多重大改进,主要有如下两点:

  1. 支持一个进程打开的socket描述符不受限制
    select最大的缺陷就是单个进程所打开的FD是有一定限制的,它由FD_SETSIZE设置,默认值是1024。对于那些需要支持上万个TCP连接的大型服务器来说显然太少了。epoll并没有这个限制,它所支持的FD上限是操作系统的最大文件句柄数,这个数字远远大于1024。例如,在1GB内存的机器上大约是10万个句柄左右。
  2. I/O效率不会随着FD数目的增加而线性下降
    传统的select/poll另一个致命弱点就是当你拥有一个很大的socket集合,由于网络延时或者链路空闲,任一时刻只有少部分的socket是“活跃”的,但是select/poll每次调用都会线性扫描全部的集合,导致效率呈现线性下降。epoll不存在这个问题,它只会对“活跃”的socket进行操作。这是因为在内核实现中epoll是根据每个fd上面的callback函数实现的,那么,只有“活跃”的socket才会主动的去调用callback函
    数,其他空闲状态socket则不会。

二.Java I/O模型

01 / IO

在1.4之前,Java只提供了一种IO模型,即阻塞式IO模型(BIO)。

在这里插入图片描述

红色:低级流
蓝色:高级流(处理低级流)

02 / NIO

新的输入/输出(NIO)库是在JDK 1.4中引入的。NIO弥补了原来同步阻塞I/O的不足,它在标准Java代码中提供了高速的、面向块的I/O。通过定义包含数据的类,以及通过以块的形式处理这些数据。NIO包含三个核心的组件:Buffer(缓冲区)、Channel(通道)、Selector(多路复用器)。

2.1 Buffer

Buffer是一个对象,它包含一些要写入或者要读出的数据。在NIO类库中加入Buffer对象,体现了新库与原I/O的一个重要区别。在面向流的I/O中,可以将数据直接写入或者将数据直接读到Stream对象中。
在NIO库中,所有数据都是用缓冲区处理的。在读取数据时,它是直接读到缓冲区中的。在写入数据时,写入到缓冲区中。任何时候访问NIO中的数据,都是通过缓冲区进行操作。

◼ Buffer是抽象类,它有如下子类:
ByteBuffer、CharBuffer、ShortBuffer、IntBuffer、LongBuffer、FloatBuffer、DoubleBuffer;
◼ 只能通过静态方法实例化Buffer:
public static ByteBuffer allocate(int capacity); // 分配堆内存
public static ByteBuffer allocateDirect(int capacity); // 分配直接内存

◼ Buffer中的四个成员变量:

  1. 容量(capacity):Buffer可以存储的最大数据量,该值不可改变;
  2. 界限(limit):Buffer中可以读/写数据的边界,limit之后的数据不能访问;
  3. 位置(position):下一个可以被读写的数据的位置(索引);
  4. 标记(mark):Buffer允许将位置直接定位到该标记处,这是一个可选属性;
    并且,上述变量满足如下的关系:0 <= mark <= position <= limit <= capacity 。

在这里插入图片描述
◼ Buffer中数据变化的过程:
在这里插入图片描述

2.2 Channel

Channel是一个通道,可以通过它读取和写入数据,它就像自来水管一样,网络数据通过Channel读取和写入。通道与流的不同之处在于通道是双向的,流只是在一个方向上移动而且通道可以用于读、写或者同时用于读写。因为Channel是全双工的,所以它可以比流更好地映射底层操作系统的API。特别是在UNIX网络编程模型中,底层操作系统的通道都是全双工的,同时支持读写操作。

◼ Channel的实现类:

  1. FileChannel:文件访问通道;
  2. SocketChannel、ServerSocketChannel:TCP通信通道;
  3. DatagramChannel:UDP通信通道;
  4. Pipe.SourceChannel、Pipe.SinkChannel:线程通信通道。

◼ Channel的实例化:

  1. 各个Channel类提供的open()方法;
  2. 字节流提供了getChannel()方法,可以直接返回FileChannel。

◼ Channel的方法:

  1. map()方法用于将Channel对应的数据映射成ByteBuffer;
  2. read()方法有一系列重载的形式,用于从Buffer中读取数据;
  3. write()方法有一系列重载的形式,用于向Buffer中写入数据。
2.3 Selector

Selector会不断地轮询注册在其上的Channel,如果某个Channel上面有新的TCP连接接入、读和写事件,这个Channel就处于就绪状态,会被Selector轮询出来,然后通过SelectionKey可以获取就绪Channel的集合,进行后续的I/O操作。

一个多路复用器Selector可以同时轮询多个Channel,由于JDK使用了epoll()代替传统的select实现,所以它并没有最大连接句柄1024/2048的限制。这也就意味着只需要一个线程负责Selector的轮询,就可以接入成千上万的客户端,这确实是个非常巨大的进步。

Selector是抽象类,可以通过调用此类的open()静态方法来创建Selector实例。Selector可以同时监控多个SelectableChannel的IO状况,是非阻塞IO的核心。一个Selector实例有三个SelectionKey集合:

  1. 所有的SelectionKey集合:代表了注册在该Selector上的Channel,这个集合可以通过keys()方法返回。
  2. 被选择的SelectionKey集合:代表了所有可通过select()方法获取的、需要进行IO处理的Channel,这个集合可以通过selectedKeys()返回。
  3. 被取消的SelectionKey集合:代表了所有被取消注册关系的Channel,在下一次执行select()方法时,这
    些Channel对应的SelectionKey会被彻底删除,程序通常无须直接访问该集合。

Selector还提供了一系列和select()相关的方法:

  1. int select():监控所有注册的Channel,当它们中间有需要处理的IO操作时,该方法返回,并将对应的SelectionKey加入被选择的SelectionKey集合中,该方法返回这些Channel的数量。
  2. int select(long timeout):可以设置超时时长的select()操作。
  3. int selectNow():执行一个立即返回的select()操作,相对于无参数的select()方法而言,该方法不会阻塞线程。
  4. Selector wakeup():使一个还未返回的select()方法立刻返回。

03 / NIO2

Java 7的NIO2提供了异步Channel支持,这种异步Channel可以提供更高效的IO,这种基于异步Channel的IO机制也被称为异步IO(AsynchronousIO)。NIO2为AIO提供了两个接口和三个实现类,其中AsynchronousSocketChannel和AsynchronousServerSocketChannel是支持TCP通信的异步Channel。

请添加图片描述

使用AsynchronousServerSocketChannel只要三步:

  1. 调用AsynchronousServerSocketChannel的静态方法open()创建AsynchronousServerSocketChannel实例。
  2. 调用AsynchronousServerSocketChannel实例的bind()方法让它在指定IP地址、端口监听。
  3. 调用AsynchronousServerSocketChannel实例的accept()方法接受连接请求。

程序调用accept()方法之后,当前线程不会阻塞,而程序也不知道accept()方法什么时候会接收到客户端
的请求。为了解决这个异步问题,AIO提供了两个版本的accept()方法。

  1. Future accept():
    接受客户端的请求。如果程序需要获得连接成功后返回的AsynchronousSocketChannel,则应该调用
    该方法返回的Future对象的get()方法。但get()方法会阻塞线程,因此这种方式依然会阻塞当前线程。
  2. void accept(A attachment, CompletionHandler<AsynchronousSocketChannel, ? super A>handler):
    接受来自客户端的请求,连接成功或连接失败都会触发CompletionHandler对象里相应的方法。
    其中AsynchronousSocketChannel就代表连接成功后返回的AsynchronousSocketChannel。

04 / Netty

在进行磁盘IO操作时,建议使用NIO。在进行网络IO操作时,则建议使用Netty框架,原因如下:

  1. NIO的类库和API繁杂,使用麻烦,你需要熟练掌握Selector、ServerSocketChannel、SocketChannel、ByteBuffer等类;
  2. 需要具备其他额外的技能做铺垫,例如熟悉Java多线程编程。这是因为NIO编程设计到Reactor模式,你必须对多线程和网络编程非常熟悉,才能编写出高质量的NIO程序;
  3. 可靠性能力补齐,工作量和难度都非常大。例如客户端面临断连重连、网络闪断、半包读写、失败缓存、网络拥塞和异常码流的处理等问题,NIO编程的特点是功能开发相对容易,但是可靠性能力补齐的工作量和难度都非常大;
  4. JDK NIO的BUG,例如臭名昭著的epoll bug,它会导致Selector空轮询,最终导致CPU 100%。官方声称在JDK 1.6版本的update18修复了该问题,但是直到JDK 1.7版本该问题仍旧存在,只不过该BUG发生的概率降低了一些而已,它并没有得到根本性的解决。

最后

2023-4-7 22:59:04

这篇博客能写好的原因是:站在巨人的肩膀上

这篇博客要写好的目的是:做别人的肩膀

开源:为爱发电

学习:为我而行

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

日星月云

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值