跳转至

Java IO 面试题(爪哇程序员)

Java 中有几种类型的流?

原文:https://zwmst.com/360.html

(1)按照流的方向:输入流(inputStream)和输出流(outputStream);

(2)按照实现 功能分:节点流(可以从或向一个特定的地方(节点)读写数据。如 FileReader)和处理流 (是对一个已存在的流的连接和封装,通过所封装的流的功能调用实现数据读写。如 BufferedReader。处理流的构造方法总是要带一个其他的流对象做参数。一个流对象经过其 他流的多次包装,称为流的链接);

(3)按照处理数据的单位: 字节流和字符流。字节流继 承于 InputStream 和 OutputStream,字符流继承于InputStreamReader 和 OutputStreamWriter 。

什么是 java序列化?

原文:https://zwmst.com/362.html

序列化就是一种用来处理对象流的机制,所谓对象流也就是将对象的内容进行流化。可以对流 化后的对象进行读写操作,也可将流化后的对象传输于网络之间。序列化是为了解决在对对象 流进行读写操作时所引发的问题

如何实现 java 序列化?

原文:https://zwmst.com/364.html

序列化的实现,将需要被序列化的类实现Serializable 接口,该接口没有需要实现的方法, implements Serializable 只是为了标注该对象是可被序列化的,然后使用一个输出流(如: File Output Stream)来构造一个 Object Output Stream(对象流)对象,接着,使用 Object Output Stream 对象的 write Object(Object obj)方法就可以将参数为 obj 的对象写出(即保 存其状态),要恢复的话则用输入流。

字节流和字符流的区别?

原文:https://zwmst.com/366.html

字节流读取的时候,读到一个字节就返回一个字节;字符流使用了字节流读到一个或多个字节 (中文对应的字节数是两个,在 UTF-8 码表中是 3 个字节)时。先去查指定的编码表,将查 到的字符返回。字节流可以处理所有类型数据,如:图片,MP3,AVI视频文件,而字符流只 能处理字符数据。只要是处理纯文本数据,就要优先考虑使用字符流,除此之外都用字节流。 字节流主要是操作 byte 类型数据,以 byte 数组为准,主要操作类就是 OutputStream、 InputStream字符流处理的单元为 2 个字节的 Unicode 字符,分别操作字符、字符数组或字 符串,而字节流处理单元为 1 个字节,操作字节和字节数组。所以字符流是由 Java 虚拟机将字 节转化为 2 个字节的 Unicode 字符为单位的字符而成的,所以它对多国语言支持性比较好! 如果是音频文件、图片、歌曲,就用字节流好点,如果是关系到中文(文本)的,用字符流好 点。在程序中一个字符等于两个字节,java 提供了 Reader、Writer 两个专门操作字符流的 类。

PrintStream、BufferedWriter、PrintWriter的比较?

原文:https://zwmst.com/368.html

  1. PrintStream类的输出功能非常强大,通常如果需要输出文本内容,都应该将输出流包 装成PrintStream后进行输出。它还提供其他两项功能。与其他输出流不同, PrintStream 永远不会抛出 IOException;而是,异常情况仅设置可通过 checkError 方法测试的内部标志。另外,为了自动刷新,可以创建一个 PrintStream

  2. BufferedWriter:将文本写入字符输出流,缓冲各个字符从而提供单个字符,数组和字符 串的高效写入。通过write()方法可以将获取到的字符输出,然后通过newLine()进行换 行操作。BufferedWriter中的字符流必须通过调用flush方法才能将其刷出去。并且 BufferedWriter只能对字符流进行操作。如果要对字节流操作,则使用 BufferedInputStream

  3. PrintWriter的println方法自动添加换行,不会抛异常,若关心异常,需要调用 checkError方法看是否有异常发生,PrintWriter构造方法可指定参数,实现自动刷新 缓存(autoflush)

什么是节点流,什么是处理流,它们各有什么用处,处理流的创建有什么特征?

原文:https://zwmst.com/370.html

  1. 节点流 直接与数据源相连,用于输入或者输出

  2. 处理流:在节点流的基础上对之进行加工,进行一些功能的扩展

  3. 处理流的构造器必须要 传入节点流的子类

流一般需要不需要关闭,如果关闭的话在用什么方法,一般要在那个代码块里面关闭比较好,处理流是怎么关闭的,如果有多个流互相调用传入是怎么关闭的?

原文:https://zwmst.com/372.html

  1. 流一旦打开就必须关闭,使用close方法

  2. 放入finally语句块中(finally 语句一定会执行)

  3. 调用的处理流就关闭处理流

  4. 多个流互相调用只关闭最外层的流

什么是BIO

原文:https://zwmst.com/374.html

BIO 就是传统的 java.io 包,它是基于流模型实现的,交互的方式是同步、阻塞方式,也就是 说在读入输入流或者输出流时,在读写动作完成之前,线程会一直阻塞在那里,它们之间的调 用时可靠的线性顺序。它的有点就是代码比较简单、直观;缺点就是 IO 的效率和扩展性很 低,容易成为应用性能瓶颈。

什么是NIO

原文:https://zwmst.com/376.html

是 Java 1.4 引入的 java.nio 包,提供了 Channel、Selector、Buffer 等新的抽象,可以构建 多路复用的、同步非阻塞 IO 程序,同时提供了更接近操作系统底层高性能的数据操作方式。

什么是AIO

原文:https://zwmst.com/378.html

AIO 是 Java 1.7 之后引入的包,是 NIO 的升级版本,提供了异步非堵塞的 IO 操作方式,所以 人们叫它 AIO(Asynchronous IO),异步 IO 是基于事件和回调机制实现的,也就是应用操 作之后会直接返回,不会堵塞在那里,当后台处理完成,操作系统会通知相应的线程进行后续 的操作

同步与异步

原文:https://zwmst.com/380.html

同步就是一个任务的完成需要依赖另外一个任务时,只有等待被依赖的任务完成后,依赖的任 务才能算完成,这是一种可靠的任务序列。要么成功都成功,失败都失败,两个任务的状态可 以保持一致。而异步是不需要等待被依赖的任务完成,只是通知被依赖的任务要完成什么工 作,依赖的任务也立即执行,只要自己完成了整个任务就算完成了。至于被依赖的任务最终是 否真正完成,依赖它的任务无法确定,所以它是不可靠的任务序列。我们可以用打电话和发短 信来很好的比喻同步与异步操作。

阻塞与非阻塞

原文:https://zwmst.com/382.html

阻塞与非阻塞主要是从 CPU 的消耗上来说的,阻塞就是 CPU 停下来等待一个慢的操作完成 CPU 才接着完成其它的事。非阻塞就是在这个慢的操作在执行时 CPU 去干其它别的事,等这 个慢的操作完成时,CPU 再接着完成后续的操作。虽然表面上看非阻塞的方式可以明显的提高 CPU 的利用率,但是也带了另外一种后果就是系统的线程切换增加。增加的 CPU 使用时间能 不能补偿系统的切换成本需要好好评估。

同步、异步、阻塞、非堵塞

原文:https://zwmst.com/384.html

同/异、阻/非堵塞的组合,有四种类型,如下表:

组 合 方 式 性能分析
同步 非 阻塞 提升 I/O 性能的常用手段,就是将 I/O 的阻塞改成非阻塞方式,尤其在网络 I/O 是长连接,同时传输数据也不是很多的情况下,提升性能非常有效。 这种方式通常能提升 I/O 性能,但是会增加CPU 消耗,要考虑增加的 I/O 性能能不能补偿 CPU 的消耗, 也就是系统的瓶颈是在 I/O 还是在 CPU 上。
异步 阻塞 这种方式在分布式数据库中经常用到,例如在网一个分布式数据库中写一条记录,通常会有一份是同步阻塞的记录,而还有两至三份是备份记录会写到其它机器上,这些 备份记录通常都是采用异步阻塞的方式写 I/O。异步阻塞对网络 I/O 能够提升效率,尤其像上面这种同时写多份相同数据的情况。
异 步非 阻塞 这种组合方式用起来比较复杂,只有在一些非常复杂的分布式情况下使用,像集群之 间的消息同步机制一般用这种 I/O 组合方式。如 Cassandra 的 Gossip 通信机制就是采用异步非阻塞的方式。它适合同时要传多份相同的数据到集群中不同的机器,同时 数据的传输量虽然不大,但是却非常频繁。这种网络 I/O 用这个方式性能能达到最高。

通道是个什么意思?

原文:https://zwmst.com/386.html

  • 通道是对原 I/O 包中的流的模拟。到任何目的地(或来自任何地方)的所有数据都必须通过 一个 Channel 对象(通道)。一个 Buffer 实质上是一个容器对象。发送给一个通道的 所有对象都必须首先放到缓冲区中;同样地,从通道中读取的任何数据都要读到缓冲区 中。

  • Channel是一个对象,可以通过它读取和写入数据。拿 NIO 与原来的 I/O 做个比 较,通道就像是流。 正如前面提到的,所有数据都通过 Buffer 对象来处理。您永远不会将字节直接写入通道 中,相反,您是将数据写入包含一个或者多个字节的缓冲区。同样,您不会直接从通道 中读取字节,而是将数据从通道读入缓冲区,再从缓冲区获取这个字节。

缓冲区是什么意思?

原文:https://zwmst.com/388.html

  • Buffer 是一个对象, 它包含一些要写入或者刚读出的数据。在 NIO 中加入 Buffer 对 象,体现了新库与原 I/O 的一个重要区别。在面向流的 I/O 中,您将数据直接写入或者 将数据直接读到 Stream 对象中
  • 在 NIO 库中,所有数据都是用缓冲区处理的。在读取数据时,它是直接读到缓冲区中 的。在写入数据时,它是写入到缓冲区中的。任何时候访问 NIO 中的数据,您都是将它 放到缓冲区中。
  • 缓冲区实质上是一个数组。通常它是一个字节数组,但是也可以使用其他种类的数组。 但是一个缓冲区不 仅仅 是一个数组。缓冲区提供了对数据的结构化访问,而且还可以跟 踪系统的读/写进程

ByteBuffer

CharBuffer

ShortBuffer

IntBuffer

LongBuffer

FloatBuffer

DoubleBuffer

IO多路复用的底层原理

原文:https://zwmst.com/390.html

IO多路复用使用两个系统调用(select/poll/epoll和recvfrom),blocking IO只调用了 recvfrom;select/poll/epoll 核心是可以同时处理多个connection,而不是更快,所以连接 数不高的话,性能不一定比多线程+阻塞IO好,多路复用模型中,每一个socket,设置为nonblocking,阻塞是被select这个函数block,而不是被socket阻塞的。

  1. select机制

客户端操作服务器时就会产生这三种文件描述符(简称fd):writefds(写)、readfds(读)、和 exceptfds(异常)。select会阻塞住监视3类文件描述符,等有数据、可读、可写、出异常 或超 时、就会返回;返回后通过遍历fdset整个数组来找到就绪的描述符fd,然后进行对应的IO操 作。

优点: 几乎在所有的平台上支持,跨平台支持性好

缺点: 由于是采用轮询方式全盘扫描,会随着文件描述符FD数量增多而性能下降。 每次调用 select(),需要把 fd 集合从用户态拷贝到内核态,并进行遍历(消息传递都是从 内核到用户空间) 默认单个进程打开的FD有限制是1024个,可修改宏定义,但是效率仍然慢。

  1. poll机制

基本原理与select一致,只是没有最大文件描述符限制,因为采用的是链表存储fd。

  1. epoll机制

epoll之所以高性能是得益于它的三个函数

  1. epoll_create()系统启动时,在Linux内核里面申请一个B+树结构文件系统,返回 epoll对象,也是一个fd
  2. epoll_ctl() 每新建一个连接,都通过该函数操作epoll对象,在这个对象里面修改添加 删除对应的链接fd, 绑定一个callback函数。
  3. epoll_wait() 轮训所有的callback集合,并完成对应的IO操作

优点: 没fd这个限制,所支持的FD上限是操作系统的最大文件句柄数,1G内存大概支持10万个 句柄 效率提高,使用回调通知而不是轮询的方式,不会随着FD数目的增加效率下降 内核和用户空间mmap同一块内存实现

33.阻塞 IO 模型

原文:https://zwmst.com/2905.html

最传统的一种 IO 模型,即在读写数据过程中会发生阻塞现象。当用户线程发出 IO 请求之后,内核会去查看数据是否就绪,如果没有就绪就会等待数据就绪,而用户线程就会处于阻塞状态,用户线程交出 CPU。当数据就绪之后,内核会将数据拷贝到用户线程,并返回结果给用户线程,用户线程才解除 block 状态。典型的阻塞 IO 模型的例子为:data = socket.read();如果数据没有就绪,就会一直阻塞在 read 方法。

34.非阻塞 IO 模型

原文:https://zwmst.com/2907.html

当用户线程发起一个 read 操作后,并不需要等待,而是马上就得到了一个结果。如果结果是一个error 时,它就知道数据还没有准备好,于是它可以再次发送 read 操作。一旦内核中的数据准备好了,并且又再次收到了用户线程的请求,那么它马上就将数据拷贝到了用户线程,然后返回。所以事实上,在非阻塞 IO 模型中,用户线程需要不断地询问内核数据是否就绪,也就说非阻塞 IO不会交出 CPU,而会一直占用 CPU。典型的非阻塞 IO 模型一般如下:

while(true){
data = socket.read();
if(data!= error){
处理数据
break;
}
}

但是对于非阻塞 IO 就有一个非常严重的问题,在 while 循环中需要不断地去询问内核数据是否就绪,这样会导致 CPU 占用率非常高,因此一般情况下很少使用 while 循环这种方式来读取数据。

35.多路复用 IO 模型

原文:https://zwmst.com/2910.html

多路复用 IO 模型是目前使用得比较多的模型。Java NIO 实际上就是多路复用 IO。在多路复用 IO模型中,会有一个线程不断去轮询多个 socket 的状态,只有当 socket 真正有读写事件时,才真正调用实际的 IO 读写操作。因为在多路复用 IO 模型中,只需要使用一个线程就可以管理多个socket,系统不需要建立新的进程或者线程,也不必维护这些线程和进程,并且只有在真正有socket 读写事件进行时,才会使用 IO 资源,所以它大大减少了资源占用。在 Java NIO 中,是通过 selector.select()去查询每个通道是否有到达事件,如果没有事件,则一直阻塞在那里,因此这种方式会导致用户线程的阻塞。多路复用 IO 模式,通过一个线程就可以管理多个 socket,只有当socket 真正有读写事件发生才会占用资源来进行实际的读写操作。因此,多路复用 IO 比较适合连接数比较多的情况。

另外多路复用 IO 为何比非阻塞 IO 模型的效率高是因为在非阻塞 IO 中,不断地询问 socket 状态时通过用户线程去进行的,而在多路复用 IO 中,轮询每个 socket 状态是内核在进行的,这个效率要比用户线程要高的多

不过要注意的是,多路复用 IO 模型是通过轮询的方式来检测是否有事件到达,并且对到达的事件逐一进行响应。因此对于多路复用 IO 模型来说,一旦事件响应体很大,那么就会导致后续的事件迟迟得不到处理,并且会影响新的事件轮询

36.信号驱动 IO 模型

原文:https://zwmst.com/2937.html

在信号驱动 IO 模型中,当用户线程发起一个 IO 请求操作,会给对应的 socket 注册一个信号函数,然后用户线程会继续执行,当内核数据就绪时会发送一个信号给用户线程,用户线程接收到信号之后,便在信号函数中调用 IO 读写操作来进行实际的 IO 请求操作。

37.异步 IO 模型

原文:https://zwmst.com/2958.html

异步 IO 模型才是最理想的 IO 模型,在异步 IO 模型中,当用户线程发起 read 操作之后,立刻就可以开始去做其它的事。而另一方面,从内核的角度,当它受到一个asynchronous read 之后,它会立刻返回,说明 read 请求已经成功发起了,因此不会对用户线程产生任何 block。然后,内核会等待数据准备完成,然后将数据拷贝到用户线程,当这一切都完成之后,内核会给用户线程发送一个信号,告诉它 read 操作完成了。也就说用户线程完全不需要实际的整个 IO 操作是如何进行的,只需要先发起一个请求,当接收内核返回的成功信号时表示 IO 操作已经完成,可以直接去使用数据了。 也就说在异步 IO 模型中,IO 操作的两个阶段都不会阻塞用户线程,这两个阶段都是由内核自动完成,然后发送一个信号告知用户线程操作已完成。用户线程中不需要再次调用 IO 函数进行具体的读写。这点是和信号驱动模型有所不同的,在信号驱动模型中,当用户线程接收到信号表示数据已经就绪,然后需要用户线程调用 IO 函数进行实际的读写操作;而在异步 IO 模型中,收到信号表示 IO 操作已经完成,不需要再在用户线程中调用 IO 函数进行实际的读写操作。

**注意,异步 IO 是需要操作系统的底层支持,在 Java 7 中,提供了 Asynchronous IO。

38.JAVA NIO

原文:https://zwmst.com/2961.html

NIO 主要有三大核心部分:Channel(通道),Buffer(缓冲区), Selector。传统 IO 基于字节流和字符流进行操作,而 NIO 基于 Channel 和 Buffer(缓冲区)进行操作,数据总是从通道读取到缓冲区中,或者从缓冲区写入到通道中。Selector(选择区)用于监听多个通道的事件(比如:连接打开,数据到达)。因此,单个线程可以监听多个数据通道

NIO 和传统 IO 之间第一个最大的区别是,IO 是面向流的,NIO 是面向缓冲区的

39.NIO 的缓冲区

原文:https://zwmst.com/2964.html

Java IO 面向流意味着每次从流中读一个或多个字节,直至读取所有字节,它们没有被缓存在任何地方。此外,它不能前后移动流中的数据。如果需要前后移动从流中读取的数据,需要先将它缓存到一个缓冲区。NIO 的缓冲导向方法不同。数据读取到一个它稍后处理的缓冲区,需要时可在缓冲区中前后移动。这就增加了处理过程中的灵活性。但是,还需要检查是否该缓冲区中包含所有您需要处理的数据。而且,需确保当更多的数据读入缓冲区时,不要覆盖缓冲区里尚未处理的数据。

40.NIO 的非阻塞

原文:https://zwmst.com/2966.html

IO 的各种流是阻塞的。这意味着,当一个线程调用 read() 或write()时,该线程被阻塞,直到有一些数据被读取,或数据完全写入。该线程在此期间不能再干任何事情了。 NIO 的非阻塞模式,使一个线程从某通道发送请求读取数据,但是它仅能得到目前可用的数据,如果目前没有数据可用时,就什么都不会获取。而不是保持线程阻塞,所以直至数据变的可以读取之前,该线程可以继续做其他的事情。 非阻塞写也是如此。一个线程请求写入一些数据到某通道,但不需要等待它完全写入,这个线程同时可以去做别的事情。 线程通常将非阻塞 IO 的空闲时间用于在其它通道上执行 IO 操作,所以一个单独的线程现在可以管理多个输入和输出通道(channel)。

41.Channel

原文:https://zwmst.com/2968.html

首先说一下 Channel,国内大多翻译成“通道”。Channel 和 IO 中的 Stream(流)是差不多一个等级的。只不过 Stream 是单向的,譬如:InputStream, OutputStream,而 Channel 是双向的,既可以用来进行读操作,又可以用来进行写操作。

NIO 中的 Channel 的主要实现有:

  1. FileChannel
  2. DatagramChannel
  3. SocketChannel
  4. ServerSocketChannel

这里看名字就可以猜出个所以然来:分别可以对应文件 IO、UDP 和 TCP(Server 和 Client)。下面演示的案例基本上就是围绕这 4 个类型的 Channel 进行陈述的。

42.Buffer

原文:https://zwmst.com/2970.html

Buffer,故名思意,缓冲区,实际上是一个容器,是一个连续数组。Channel 提供从文件、网络读取数据的渠道,但是读取或写入的数据都必须经由 Buffer。

上面的图描述了从一个客户端向服务端发送数据,然后服务端接收数据的过程。客户端发送数据时,必须先将数据存入 Buffer 中,然后将 Buffer 中的内容写入通道。服务端这边接收数据必须通过 Channel 将数据读入到 Buffer 中,然后再从 Buffer 中取出数据来处理。在 NIO 中,Buffer 是一个顶层父类,它是一个抽象类,常用的 Buffer 的子类有:

ByteBuffer、IntBuffer、 CharBuffer、 LongBuffer、 DoubleBuffer、FloatBuffer、ShortBuffer

43.Selector

原文:https://zwmst.com/2976.html

Selector 类是 NIO 的核心类,Selector 能够检测多个注册的通道上是否有事件发生,如果有事件发生,便获取事件然后针对每个事件进行相应的响应处理。这样一来,只是用一个单线程就可以管理多个通道,也就是管理多个连接。这样使得只有在连接真正有读写事件发生时,才会调用函数来进行读写,就大大地减少了系统开销,并且不必为每个连接都创建一个线程,不用去维护多个线程,并且避免了多线程之间的上下文切换导致的开销。


我们一直在努力

apachecn/AiLearning

【布客】中文翻译组