【Java面试题】IO & NIO

1、什么是IO流?

它是一种数据的流从源头流到目的地。比如文件拷贝,输入流和输出流。输入流从文件中读取数据存储到进程(process)中,输出流从进程中读取数据然后写入到目标文件。

2、Java中有几种类型的流?

  • 按照单位大小:字符流、字节流。
  • 按照流的方向:输出流、输入流。

3、字节流和字符流哪个好?怎么选择?

  • 绝大多数情况下使用字节流会更好,因为字节流是字符流的包装,而大多数时候 IO 操作都是直接操作磁盘文件,所以这些流在传输时都是以字节的方式进行的〔图片等都是按字节存储的)
  • 如果操作是需要通过 IO 在内存中频繁处理字符串的情况,使用字符流会好些,因为字符流具备缓冲区,提高了性能。

4、读取数据量大的文件时,速度会很慢,如何选择流?

字节流时,选择BufferedInputStreamBufferedOutputStream
字符流时,选择BufferedReaderBufferedWriter

5、IO 模型有几种?

  • 阻塞 IO
  • 非阻塞 IO
  • 多路复用 IO
  • 信号驱动 IO
  • 异步 IO

6、阻塞IO(blocking IO)

应用程序调用一个 IO 函数,导致应用程序阻塞,如果数据已经准备好,从内核拷贝到用户空间,否则一直等待下去。一个典型的读操作流程大致如下图,当用户进程调用 recvfrom 这个系统调用时,kernel 就开始了 IO 的第一个阶段:准备数据,就是数据被拷贝到内核缓冲区中的一个过程(很多网络 IO 数据不会那么快到达,如没收一个完整的UDP包),等数据到操作系统内核缓冲区了,就到了第二阶段:将数据从内核缓冲区拷贝到用户内存,然后 kernel 返回结果,用户进程才会解除 block 状态,重新运行起来。

blocking IO 的特点就是在 IO 执行的两个阶段用户进程都会 block 住;

在这里插入图片描述

7、非阻塞 I/O(non-blocking IO)

非阻塞 I/O 模型,我们把一个套接口设置为非阻塞就是告诉内核,当所请求的 I/O 操作无法完成时,不要将进程睡眠,而是返回一个错误。这样我们的 I/O 操作函数将不断地测试数据是否已经准备好,如果没有准备好,继续测试,直到数据准备好为止。在这个不断测试的过程中,会大量的占用CPU的时间。

当用户进程发出read操作时,如果kernel中数据还没准备好,那么并不会 block 用户进程,而是立即返回error,用户进程判断结果是error,就知道数据还没准备好,用户可以再次发read,直到kernel中数据准备好,并且用户再一次发read操作,产生system call,那么kernel马上将数据拷贝到用户内存,然后返回;所以nonblocking I/O 的特点是用户进程需要不断的主动询问 Kernel 数据好了没有。

阻塞 I/O 一个线程只能处理一个 I/O 流事件,要想同时处理多个 I/O 流事件要么多线程要么多进程,这样做效率显然不会高,而非阻塞 I/O 可以一个线程处理多个流事件,只要不停地询所有流事件即可,当然这个方式也不好,当大多数流没数据时,也是会大量浪费CPU资源;为了避免CPU空转,引进代理(select 和 poll ,两种方式相差不大),代理可以观察多个流 I/O 事件,空闲时会把当前线程阻塞掉,当有一个或多个 I/O 事件时,就从阻塞态醒过来,把所有 I/O 流都轮询一遍,于是没有 I/O 事件我们的程序就阻塞在 select 方法处,即便这样依然存在问题,我们从 select 处只是知道有 I/O 事件发生,却不知道是哪几个流,还是只能轮询所有流,epoll这样的代理就可以把哪个流发生怎样的 I/O 事件通知我们:
在这里插入图片描述

8、I/O多路复用模型(I/O multiplexing)

I/O 多路复用就在于单个进程可以同时处理多个网络连接 IO,基本原理就是select、poll、epoll这些个函数会不断轮询所负责的所有socket,当某个socket有数据到达了,就通知用户进程,这三个function会阻塞进程,但和 I/O 阻塞不同,这些函数可以同时阻塞多个 I/O 操作,而且可以同时对多个读操作,写操作 I/O 进行检验,直到有数据到达,才真正调用 I/O 操作函数,调用过程如下图;

所以 I/O 多路复用的特点是通过一种机制一个进程能同时等待多个文件描述符,而这些文件描述符(套接字描述符)其中任意一个进入就绪状态,select函数就可以返回。

I/O 多路复用的优势在于适合并发数比较高的 I/O 操作清况,可以同时处理多个连接,和bloking I/O 一样,socket 是被阻塞的,只不过在多路复用中 socket 是被 select 阻塞,而在阻塞 I/O 中是被socket I/O 给阻塞。

在这里插入图片描述

9、信号驱动I/O模型

可以用信号,让内核在描述符就绪时发送 SIGIO 信号通知我们,通过 sigaction 系统调用安装一个信号处理函数。该系统调用将立即返回,我们的进程继续工作,也就是说它没有被阻塞。当数据报准备好读取时,内核就为该进程产生一个 SIGIO 信号。我们随后既可以在信号处理函数中调用recvfrom读取数据报,并通知主循环数据已经准备好待处理。特点:等待数据报到达期间进程不被阻塞。主循环可以继续执行,只要等待来自信号处理函数的通知:既可以是数据已准备好被处理,也可以是数据报已准备好被读取。

在这里插入图片描述

10、异步I/O(asynchronous IO)

异步 I/O 告知内核启动某个操作,并让内核在整个操作(包括将内核数据复制到我们自己的缓冲区)完成后通知我们,调用aio_read(Posix异步 I/O 函数以 aio 或 lio 开头)函数,给内核传递描述字、缓冲区指针、缓冲区大小(与read相同的3个参数)、文件偏移以及通知的方式,然后系统立即返回。我们的进程不阻塞于等待 I/O 操作的完成。当内核将数据拷贝到缓冲区后,再通知应用程序。

用户进程发起 read 操作之后,立刻就可以开始去做其它的事。而另一方面,从 kernel 的角度,当它受到一个 asynchronous read 之后,首先它会立刻返回,所以不会对用户进程产生任何 block 。然后,kernel 会等待数据准备完成,然后将数据拷贝到用户内存,当这一切都完成之后,kernel会给用户进程发送一个 signal,告诉它read操作完成了。
在这里插入图片描述

11、NIO 与 IO 的区别?

NIO 即 New IO,这个库是在JDK1.4中才引入的。NIO 和 IO 有相同的作用和目的,但实现方式不同,NIO 主要用到的是块,所以 NIO 的效率要比 IO 高很多。在Java API中提供了两套 NIO ,一套是针对标准输入输出 NIO,另一套就是网络编程 NIO。

IONIO
面向流面向缓冲
阻塞IO非阻塞IO
选择器

12、NIO和IO适用场景

NIO 是为弥补传统 IO 的不足而诞生的,但是尺有所短寸有所长,NIO 也有缺点,因为NIO是面向缓冲区的操作,每一次的数据处理都是对缓冲区进行的,那么就会有一个问题,在数据处理之前必须要判断缓冲区的数据是否完整或者已经读取完毕,如果没有,假设数据只读取了一部分,那么对不完整的数据处理没有任何意义。所以每次数据处理之前都要检测缓冲区数据。

那么 NIO 和 IO 各适用的场景是什么呢?

如果需要管理同时打开的成千上万个连接,这些连接每次只是发送少量的数据,例如聊天服务器,这时候用 NIO 处理数据可能是个很好的选择。

而如果只有少量的连接,而这些连接每次要发送大量的数据,这时候传统的 IO 更合适。使用哪种处理数据,需要在数据的响应等待时间和检查缓冲区数据的时间上作比较来权衡选择。

13、NIO核心组件

channel、buffer、selector

14、什么是channel

一个Channel(通道)代表和某一实体的连接,这个实体可以是文件、网络套接字等。也就是说,通道是 Java NIO 提供的一座桥梁,用于我们的程序和操作系统底层 I/O 服务进行交互。

通道是一种很基本很抽象的描述,和不同的 I/O 服务交互,执行不同的 I/O 操作,实现不一样,因此具体的有FileChannel、SocketChannel等。

通道使用起来跟Stream比较像,可以读取数据到 Buffer 中,也可以把 Buffer 中的数据写入通道。

在这里插入图片描述
当然,也有区别,主要体现如下两点:

  • 一个通道,既可以读又可以写,而一个Stream是单向的(所以分为InputStream和OutputStream)
  • 通道有非阻塞 I/O 模式。

15、Java NIO 中最常用的通道实现?

  • FileChannel:读写文件
  • DatagramChannel:UDP协议网络通信
  • SocketChannel:TCP协议网络通信
  • ServerSocketChannel:监听TCP连接

16、Buffer是什么?

NIO 中所使用的缓冲区不是一个简单的byte数组,而是封装过的Buffer类,通过它提供的API,我们可以灵活的操纵数据。

与Java基本类型相对应,NIO 提供了多种Buffer类型,如ByteBuffer、CharBuffer、IntBuffer等,区别就是读写缓冲区时的单位长度不一样(以对应类型的变量为单位进行读写)。

17、核心Buffer实现有哪些?

核心的 buffer 实现有这些:ByteBuffer、CharBuffer、DoubleBuffer、FloatBuffer、IntBuffer、LongBuffer、ShortBuffer,涵盖了所有的基本数据类型〔4类8种,除了Boolean)。也有其他的buffer,如MappedByteBuffer。

18、buffer读写数据基本操作

  1. 将数据写入 buffer
  2. 调用buffer.flip()
  3. 将数据从 buffer 中读取出来
  4. 调用 buffer.clear() 或者 buffer.compact()

在写buffer的时候,buffer会跟踪写入了多少数据,需要读buffer的时候,需要调用 flip() 来将buffer从写模式切换成读模式,读模式中只能读取写入的数据,而非整个 buffer。

当数据都读完了,你需要清空buffer以供下次使用,可以有2种方法来操作:调用 clear() 或者调用 compact()

区别:clear方法清空整个buffer,compact方法只清除你已经读取的数据,未读取的数据会被移到buffer的开头,此时写入数据会从当前数据的末尾开始。

// 创建一个容量为48的ByteBuffer
ByteBuffer buf = ByteBuffer.allocate(48);
// 从channel中读(取数据然后写)入buffer
int bytesRead = inChannel.read(buf);
// 下面是读取buffer
while (bytesRead != -1) {
	buf.flip(); // 转换buffer为读模式
	System.out.print((char) buf.get()); // 一次读取一个byte
	buf.clear(); //清空buffer准备下一次写入
}

19、Selector是什么?

Selector(选择器)是一个特殊的组件,用于采集各个通道的状态(或者说事件)。我们先将通道注册到选择器,并设置好关心的事件,然后就可以通过调用select()方法,静静地等待事件发生。

20、通道可以监听哪几个事件?

通道有如下4个事件可供我们监听:

  • Accept:有可以接受的连接
  • Connect:连接成功
  • Read:有数据可读
  • Write:可以写入数据了

21、为什么要用Selector?

如果用阻塞 I/O,需要多线程(浪费内存),如果用非阻塞 I/O 需要不断重试(耗费CPU)。Selector的出现解决了这尴尬的问题,非阻塞模式下,通过Selector,我们的线程只为已就绪的通道工作,不用盲目的重试了。比如,当所有通道都没有数据到达时,也就没有Read事件发生,我们的线程会在select()方法处被挂起,从而让出了CPU资源。

22、Selector处理多Channel图文说明

在这里插入图片描述
要使用一个Selector,你要先注册这个Selector的Channels。然后你调用Selector的select()方法。这个方法会阻塞,直到它注册的 Channels 当中有一个谁备好了的事件发生了。当 select() 方法返回的时候,线程可以处理这些事件,如新的连接的到来,数据收到了等。

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值