IO

Java 的四大基础部分:java 基础技术、 java 集合、 JVM、 多线程和并发至此已经讲解结束。此外,Java 还有一些其他稍散的面试主题,剩下的两个章节会分别围绕着 IO 和 队列进行展开说明。

一、IO 知识结构及面试题目分析

IO 即 Input/Output(输入 / 输出),一般的读写过程为:

  • 从磁盘读取数据:磁盘–> 内核空间 --> 用户空间
  • 从磁盘写入数据:用户空间–> 内核空间 --> 磁盘

可以看到,IO 操作通常和底层操作系统结合得比较紧密,一般的 Java 面试不会考察得太深入,通常以考察 IO 中的概念为主。 也有部分岗位(比如说 RPC 中间件、网络开发)和部分公司(比如说腾讯比较喜欢问网络方面的题目)会问得比较深入,这通常会往两个方向 深入: 一是和实战或者框架结合,预设一个场景回答实际中的问题;二是和操作系统结合,考察底层实现。

二、典型面试例题及思路分析

问题 1:"Java 中 BIO、NIO、AIO 的区别是什么?

含义不同:

  • BIO(Blocking IO)是同步并阻塞的 IO,线程发起 IO 请求后,不论内核是否准备好 IO 操作,都会一直阻塞直到操作完成;
  • NIO(Non-blocking IO)是同步非阻塞的 IO,线程发起 IO 请求后立即返回;内核在做好 IO 操作的准备之后,通过调用注册的回调函数通知线程做 IO 操作,线程开始阻塞,直到操作完成;
  • AIO(Asynchronous IO)是异步非阻塞的 IO,线程发起 IO 请求,立即返回;内存做好 IO 操作的准备之后,做 IO 操作,直到操作完成或者失败,通过调用注册的回调函数通知线程做 IO 操作完成或者失败。

应用场景不同:

  • BIO 从 JDK1.4 之前的版本,适用于低负载、低并发、业务逻辑耗时较长的场景;
  • NIO 从 JDK1.4 开始支持,适用于高负载高并发且业务逻辑简单(轻操作)的场景,典型场景是聊天服务器;
  • AIO 从 JDK1.7 开始支持,适用于高负载高并发且业务逻辑复杂(重操作)的场景,典型场景是相册服务器。

点评:

从 Java 发展历史来看,BIO->NIO->AIO,后者的出现是为了扩展前者的应用场景。

  • BIO:在服务端,通常是在 while 循环中调用 accept 方法等待接收客户端的连接请求,一旦接收到一个连接请求,就可以建立通信套接字在这个通信套接字上进行读写操作,此时不能再接收其他客户端连接请求,只能等待同当前连接的客户端的操作执行完成。 如果 BIO 要能够同时处理多个客户端请求,就必须使用多线程,即每次 accept 阻塞等待来自客户端请求,一旦受到连接请求就建立通信套接字同时开启一个新的线程来处理这个套接字的数据读写请求,然后立刻又继续 accept 等待其他客户端连接请求,即为每一个客户端连接请求都创建一个线程来单独处理;
  • NIO :与 BIO 的最大的区别是多路复用的思想,只需要开启一个线程就(或者少量多线程)可以处理来自多个客户端的 IO 事件,用来扩展 BIO 的高并发场景。
    若服务端监听到客户端连接请求,便为其建立通信套接字 (java 中就是通道),然后返回继续监听,若同时有多个客户端连接请求到来也可以全部收到,依次为它们都建立通信套接字。
    若服务端监听到来自已经创建了通信套接字的客户端发送来的数据,就会调用对应接口处理接收到的数据,若同时有多个客户端发来数据也可以依次进行处理。
  • AIO:与 NIO 不同,当进行读写操作时,只须直接调用 API 的 read 或 write 方法即可。这两种方法均为异步的,完成后会主动调用回调函数。如果是读操作,操作系统会将可读的流传入 read 方法的缓冲区,并通知应用程序;如果是写操作,操作系统在将 write 方法传递的流写入完成后,也会通知应用程序。 在 JDK1.7 中,主要在 java.nio.channels 包下增加了下面四个异步通道来实现:
  • AsynchronousSocketChannel
  • AsynchronousServerSocketChannel
    • AsynchronousFileChannel
    • AsynchronousDatagramChannel
      这四个类的 read/write 方法,都会返回一个带回调函数的对象,当执行完读取 / 写入操作后,直接调用回调函数。
问题 2:" 同步与异步、阻塞与非阻塞的区别是什么?
  • 同步与异步:
    • 同步:调用线程发出同步请求后,在没有得到结果前,该调用就不会返回。前面的同步调用处理完了后才能处理下一个同步调用。
    • 异步:调用线程发出异步请求后,在没有得到结果前,该调用就返回了。真正的结果数据会在业务处理完成后以信号或者回调的形式通知调用者。
  • 阻塞与非阻塞
    • 阻塞:调用线程发出请求后,在没有得到结果前,该线程就会被挂起,此时该线程处于非可执行状态。直到返回结果返回后,此线程才会被唤醒,继续运行。
    • 非阻塞:调用线程发出请求后,在没有得到结果前,该调用就返回了,整个过程该线程不会被挂起。

点评:

同步与异步、阻塞与非阻塞很容易混淆。简单地说:

  • 同步 / 异步的关键差异在于 IO 操作就绪后有没有回调,是针对数据处理端而言;
    • 同步 ,IO 操作是否就绪后没有回调,通常需要业务发起端使用轮询方法查看;
    • 异步 ,IO 操作是否就绪后有回调,通常会主动唤起业务发起端;
    • 阻塞 / 非阻塞的关键差异在于 IO 操作尚未就绪时,进程是否需要等待,是针对业务发起端而言;
      • 阻塞,IO 操作沿未就绪时,业务发起端的用户程序就进入等待状态,直到数据准备好或者可以读写为止。
      • 非阻塞,IO 操作沿未就绪时,业务发起端的用户程序可以立刻返回无须等待。

举个例子,张三煮开水。场景:张三、普通水壶(下称水壶)、水开后会响的水壶(下称响水壶)。

  • 同步阻塞: 张三把水壶放到火上,在旁边等水开;
  • 同步非阻塞:张三把水壶放到火上,去客厅看电视,时不时去厨房看看水开没有;
  • 异步阻塞:张三把响水壶放到火上,在旁边等水开。
  • 异步非阻塞:张三把响水壶放到火上,去客厅看电视,水壶响之前不再去看它了,响了再去拿壶。

所谓同步异步,是针对水壶(数据处理端)而言:普通水壶,同步,只能让调用者(张三)去轮询水壶开没有;响水壶,异步,水开后自己会发信号给张三。
所谓阻塞非阻塞,是针对张三而言:在旁边等待的张三,阻塞 , 别人叫他也干不了其他事;看电视的张三,非阻塞,烧水的同时还可以看电视。

问题 3:"字节流、字符流的区别及适用场景分别是什么?"
  • 区别:
    • 处理单元不同,J 字节流处理的最基本单位为 1 个字节,字符流处理的最基本的单元是 Unicode 代码单元(大小 2 字节);
    • 字节流默认不使用缓冲区;字符流使用缓冲区;
  • 适用场景
    • 字节流实际上可以处理任何文件,因为字节是存储的基础单元;而待处理的流如果是可打印的字符,那么用字符流更方便一些。

点评:

JDK 中, 字节流操作一般都是 InputStream, OutputStream 以及各种包装类;而字符流字符操作一般使用 Writer,Reader 等,可以参考下面的类图
图片描述
由于 Java 内用 Unicode 编码存储字符,而外部文件的编码其实多种多样,多字节的字符还可能占用多个字符,比如 GBK 的汉字就占用 2 个字节,而 UTF-8 的汉字就占用 3 个字节。因此字符流处理类还负责处理外部其他编码的字符流和 java 内 Unicode 字符流之间的转换。

这个题目虽然看起来是封闭式的题目,但其实也是可深入的,比如说区别是提到的缓冲区,那么什么是缓冲区?又有什么作用呢?

  • 可以把缓冲区理解为一段特殊的内存,字符流操作时为了不那么频繁地读写 IO,会把一部分数据暂时读入到内存的某块区域,然后直接从该区域读取数据,以提升程序的性能。在字符流的操作中,所有的字符都是在内存中形成的,从而使用缓冲区暂存数据。如果想在不关闭时也可以将字符流的内容全部输出,则可以使用 Writer 类中的 flush () 方法完成。

三、总结

本章节主要讲述了 IO 部分的三个很重要的基础概念:同步 / 异步和阻塞 / 非阻塞、BIO/NIO/AIO 以及字节流 / 字符流。基于这部分的理解,IO 还可以和通信框架(比如说 Netty/MINA)以及操作系统结合,这是另外一个可以写成一本书的庞大话题,扩展阅读中有一个入门,如果需要深入了解的话,可以参考相关著作。

四、扩展阅读及思考题

问:什么是流?作用是什么?

流是一种有顺序的,有起点和终点的字节集合,是对数据传输的总成或抽象。即数据在两设备之间的传输称之为流,流的本质是数据传输,根据数据传输的特性讲流抽象为各种类,方便更直观的进行数据操作。

问:IO流的分类

根据数据处理类的不同分为:字符流和字节流;

根据数据流向不同分为:输入流和输出流。

问:字符流和字节流的区别是区别是什么?

字符流的由来:因为数据编码的不同,而有了对字符进行高效操作的流对象,其本质就是基于字节流读取时,去查了指定的码表。

字符流和字节流的区别:

(1)读写单位不同:字节流一字节(8bit)为单位,字符流以字符为单位,根据码表映射字符,一次可能读多个字节。

(2)处理对象不同:字节流能处理所有类型的数据(例如图片,avi),而字符流只能处理字符类型的数据。

(3)字节流操作的时候本身是不会用到缓冲区的,是对文件本身的直接操作。而字符流在操作的时候是会用到缓冲区的,通过缓冲区来操作文件。

结论:优先使用字节流,首先因为在硬盘上所有的文件都是以字节的形式进行传输或保存的,包括图片等内容。但是字符流只是在内存中才会形成,所以在开发中字节流使用广泛。

问:什么是java序列化,如何实现java序列化?

Java对象的序列化指将一个java对象写入OI流中,与此对应的是,对象的反序列化则从IO流中恢复该java对象。 如果要让某个对象支持序列化机制,则必须让它的类是可序列化的,为了让某个类是可序列化的,该类必须实现Serializable接口或Externalizable接口

问:读写原始数据,采用什么流?

InputStream/OutputStream

问:为了提高读写性能,采用什么流?

BufferedInputStream/BufferedOutputStream

问:对各种基本数据类型和String类型的读写,采用什么流?

DataInputStream/DataOutputStream

问:指定字符编码,采用什么流?

InputStreamReader/OutputStreamWriterå

问:Linux常见IO模型有哪些?

linux下有五种常见的IO模型:

1、阻塞 I/O(blocking IO)

2、非阻塞 I/O(nonblocking IO)

3、I/O 多路复用( IO multiplexing)

4、信号驱动 I/O( signal driven IO)

5、异步 I/O(asynchronous IO)

只有5是异步模型,其余皆为同步模型。

在这里插入图片描述

问:什么是阻塞IO模型?

阻塞IO模型是最常见的IO模型了,对于所有的“慢速设备”(socket、pipe、fifo、terminal)的IO默认的方式都是阻塞的方式。阻塞就是进程放弃cpu,让给其他进程使用cpu。进程阻塞最显著的表现就是进程睡眠了。阻塞的时间通常取决于数据是否到来。 这种方式使用简单,但随之而来的问题就是会形成阻塞,需要独立线程配合,而这些线程在大多数时候都是没有进行运算的。Java的BIO使用这种方式,问题带来的问题很明显,一个Socket需要一个独立的线程,因此,会造成线程膨胀。

在这里插入图片描述

问:什么是非阻塞IO模型?

非阻塞IO就是设置IO相关的系统调用为non-blocking,随后进行的IO操作无论有没有可用数据都会立即返回,并设置errno为EWOULDBLOCK或者EAGAIN。我们可以通过主动check的方式(polling,轮询)确保IO有效时,随之进行相关的IO操作。当然这种方式看起来就似乎不太靠谱,浪费了太多的CPU时间,用宝贵的CPU时间做轮询太不靠谱儿了。

在这里插入图片描述

问:什么是多路复用IO模型?

为了解决阻塞I/O的问题,就有了I/O多路复用模型,多路复用就是用单独的线程(是内核级的, 可以认为是高效的优化的) 来统一等待所有的socket上的数据, 一当某个socket上有数据后, 就启用用户线程(可能是从线程池中取出, 而不是重新生成), copy socket data, 并且处理message.因为网络延迟的原因, 同时在处理socket data的用户线程往往比实际的socket数量要少很多. 所以实际应用中, 大部分是用线程池, 池中thread数量可随socket的高峰和低谷 而动态调整.

多路复用I/O中内核中统一的wait socket data那部分可以理解成是非阻塞, 也可以理解成阻塞. 可以理解成非阻塞 是因为它不是等到socket数据全部到达再处理, 而是有了一部分数据就会调用用户线程来处理, 理解成阻塞, 是因为它和用户空间(Appliction)层的非阻塞socket的不同是: socket中没有数据时, 内核还是wait(阻塞)的, 而用户空间的非阻塞socket没有数据也会返回, 会造成CPU的浪费.

Linux下的select和poll 就是多路复用模式,poll相对select,没有了句柄数的限制,但他们都是在内核层通过轮询socket句柄的方式来实现的, 没有利用更底层的notify机制. 但就算是这样,相对阻塞socket也已经进步了很多很多了! 毕竟用一个内核线程就解决了,阻塞socket中N多线程都在无谓地wait的局面.

多路复用I/O 还是让用户层来copy socket data. 这个过程是将内核中的socket buffer copy到用户空间的 buffer. 这有两个问题: 一是多了一次内核空间switch到用户空间的过程, 二是用户空间层不便暴露很低层但很高效的copy方式(比如DMA), 所以如果由内核层来做这个动作, 可以更好地提高效率!

在这里插入图片描述

问:什么是信号驱动IO模型?

所谓信号驱动,就是利用信号机制,安装信号SIGIO的处理函数(进行IO相关操作),通过监控文件描述符,当其就绪时,通知目标进程进行IO操作(signal handler)。

扩展阅读_信号驱动IO模型 .png

问:什么是异步IO模型?

由于异步IO请求只是写入了缓存,从缓存到硬盘是否成功不可知,因此异步IO相当于把一个IO拆成了两部分,一是发起请求,二是获取处理结果。因此,对应用来说增加了复杂性。但是异步IO的性能是所有很好的,而且异步的思想贯穿了IT系统方方面面。

扩展阅读_异步IO模型 .png

问:select、poll、epoll的区别是什么?

1、支持一个进程所能打开的最大连接数

  • select 是 32位机默认是1024个,64位机默认是2048。
  • poll 本质上于select没有区别,但是它没有最大连接数的限制,原因是它是基于链表来存储的。
  • epoll 虽然有连接数上限,但是很大,1G内存的机器上可以打开10万左右的连接,2G内存的可以打开20万左右的连接。

2、fd剧增后带来的I/O效率问题

  • select 每次调用事都会对连接进行线性遍历,所以随着fd的增加会造成遍历速度慢,呈线性下降性能问题。
  • poll 同上
  • epoll epoll内核中实现是根据每个fd的callback函数来实现的,只有活跃的socket才会主动调用callback,所以在活跃的socket较少的情况下,使用epoll没有前面两者的线性下降的性能问题,但是所有的socket都很活跃的情况下,可能会有性能问题。

3、消息传递方式

  • select 内核需要将消息传递到用户空间,都需要内核拷贝动作
  • poll 同上
  • epoll 通过内核和用户空间共享一块内存来实现的。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值