IO流

Java IO 流了解吗?

Java 的 IO(输入/输出)流是用于处理输入和输出数据的类库,通过流,程序可以从各种输入源(如文件、网络)读取数据,或将数据写入目标位置(如文件、控制台)。IO 流在 Java 中分为输入流和输出流,而根据数据的处理方式又分为字节流和字符流。

  • 字节流:处理8位字节数据,适合于处理二进制文件,如图片、视频等。主要类是Inputstream和 outputstream 及其子类。

  • 字符流:处理 16 位字符数据,适合于处理文本文件。主要类是Reader和 writer 及其子类。

Java IO 流的 40 多个类都是从如下 4 个抽象类基类中派生出来的。

  • InputStream/Reader: 所有的输入流的基类,前者是字节输入流,后者是字符输入流。

  • OutputStream/Writer: 所有输出流的基类,前者是字节输出流,后者是字符输出流。

I/O 流为什么要分为字节流和字符流呢?

问题本质想问:不管是文件读写还是网络发送接收,信息的最小存储单元都是字节,那为什么 I/O 流操作要分为字节流操作和字符流操作呢?

主要有两点原因:

  • 字符流是由 Java 虚拟机将字节转换得到的,这个过程还算是比较耗时;

  • 如果不知道编码类型的话,使用字节流的过程中很容易出现乱码问题。

Java IO流 中的设计模式有哪些?

设计模式详细请查看:设计模式概述

  1. 装饰者模式

IO流中的包装类使用到了装饰者模式。BufferedInputStream,BufferedOutputStream,BufferedReader,BufferedWriter。

我们以BufferedWriter举例来说明,先看看如何使用BufferedWriter

看它们的结构: BufferedWriter使用装饰者模式对Writer子实现类进行了增强,添加了缓冲区,提高了写数据的效率

  1. 适配器模式

Reader(字符流)、InputStream(字节流)的适配使用的是InputStreamReader。

InputStreamReader继承自java.io包中的Reader,对他中的抽象的未实现的方法给出实现。如:

如上代码中的sd(StreamDecoder类对象),在Sun的JDK实现中,实际的方法实现是对sun.nio.cs.StreamDecoder类的同名方法的调用封装。类结构图如下: 从上图可以看出:

  • InputStreamReader是对同样实现了Reader的StreamDecoder的封装。

  • StreamDecoder不是Java SE API中的内容,是Sun JDK给出的自身实现。但我们知道他们对构造方法中的字节流类(InputStream)进行封装,并通过该类进行了字节流和字符流之间的解码转换。

从表层来看,InputStreamReader做了InputStream字节流类到Reader字符流之间的转换。而从如上Sun JDK中的实现类关系结构中可以看出,是StreamDecoder的设计实现在实际上采用了适配器模式。

  1. 工厂模式

工厂模式用于创建对象,NIO 中大量用到了工厂模式,比如 Files 类的 newInputStream 方法用于创建 InputStream 对象(静态工厂)、 Paths 类的 get 方法创建 Path 对象(静态工厂)、ZipFileSystem 类(sun.nio包下的类,属于 java.nio 相关的一些内部实现)的 getPath 的方法创建 Path 对象(简单工厂)。

  1. 观察者模式

NIO 中的文件目录监听服务使用到了观察者模式。

NIO 中的文件目录监听服务基于 WatchService 接口和 Watchable 接口。WatchService 属于观察者,Watchable 属于被观察者。

Java 写入文件到磁盘会经历哪些过程?

这个问题虽然主要考察对操作系统缓存、文件系统和磁盘的理解,不过最好在开头再提一下Java 中的操作,表现出从应用层到底层 DMA 的全方位理解。

  1. 写入文件缓冲区:为了提高效率,写入文件都会利用缓冲区,即当写入数据时,数据先被存储到缓冲区,而不是直接写入磁盘。缓冲区的目的是减少磁盘 I/O 操作的次数,提高性能;一般用的是 BufferedOutputstream或BufferedWriter类

  2. write 系统调用(不是Java 中的 write):当缓冲区写满了,或者Java程序主动调用 flush()或 close()时,会触发相应的系统调用write(),将数据从用户态传递到内核态。

  3. 操作系统接收到系统调用后,会将数据写入内核缓冲区(通常称为Page Cache)。这时,数据还在内存中,并未立即写入磁盘。

  4. DMA 拷贝:操作系统中的 I/O调度器决定实际的写入顺序,等到调度后,数据从内核缓冲区通过 DMA(Direct Memory Access)或中断机制传递到磁盘控制器的硬件缓冲区

  5. 磁盘控制器最终将数据写入到磁盘的物理扇区中(磁盘上的数据写入可能涉及到旋转延迟、寻道时间等物理过程)。

如果 Java 文件写入想直接刷盘而不是先被写到页缓存中,可以执行Java提供了原子文件操作(Filechannel的force() 方法),确保写入的内容在物理磁盘上持久化,并且避免操作系统的缓存延迟。

网络编程

同步和异步的区别?

同步:发出一个调用时,在没有得到结果之前,该调用就不返回。

异步:在调用发出后,被调用者返回结果之后会通知调用者,或通过回调函数处理这个调用。

阻塞和非阻塞的区别?

阻塞和非阻塞关注的是线程的状态。

阻塞调用是指调用结果返回之前,当前线程会被挂起。调用线程只有在得到结果之后才会恢复运行。

非阻塞调用指在不能立刻得到结果之前,该调用不会阻塞当前线程。

举个例子,理解下同步、阻塞、异步、非阻塞的区别:

同步就是烧开水,要自己来看开没开;异步就是水开了,然后水壶响了通知你水开了(回调通知)。阻塞是烧开水的过程中,你不能干其他事情,必须在旁边等着;非阻塞是烧开水的过程里可以干其他事情。

Java中常见的3种IO模型? BIO/NIO/AIO的区别?

同步阻塞IO(Blocking I/O) : 用户进程发起一个IO操作以后,必须等待IO操作的真正完成后,才能继续运行。

同步非阻塞IO (Non-blocking/New I/O): 客户端与服务器通过Channel连接,采用多路复用器轮询注册的Channel。提高吞吐量和可靠性。用户进程发起一个IO操作以后,可做其它事情,但用户进程需要轮询IO操作是否完成,这样造成不必要的CPU资源浪费。Java 中的 NIO 可以看作是 I/O 多路复用模型

异步非阻塞IO(Asynchronous I/O): 非阻塞异步通信模式,NIO的升级版,采用异步通道实现异步通信,其read和write方法均是异步方法。用户进程发起一个IO操作,然后立即返回,等IO操作真正的完成以后,应用程序会得到IO操作完成的通知。类似Future模式。

NIO中的核心组件有哪些?

  • 通道(Channel): 就像是连接你的代码和文件、套接字的管道,可以读取和写入数据。你可以在通道上进行读写操作。

  • 缓冲区(Buffer): 这是用来存放数据的容器,可以把数据写入缓冲区,然后从中读取数据。就像是你盛菜的盘子,你可以把菜装进去,也可以从里面取菜。

  • 选择器(Selector): 这是NIO的强大之处,它可以监视多个通道的状态,例如是否有数据可读或可写。这样可以在一个线程中管理多个通道,提高效率。

  • 通道适配器(Channel Adapter): 这是一些特殊类型的通道,可以连接不同类型的IO接口,比如文件IO和网络IO。

什么是 Channel?

Channel 是Java NIO(New I/O)中的一个核心概念,用于数据的读写操作,它提供了一种比传统流更高效的I/O操作方式:

  • Channel 是双向的,可以同时支持读取和写入(读/写),与传统的I/O流相比更灵活。传统的流只能单向,要么是输入流要么是输出流

  • 常用于非阻塞I/O操作,可以结合Selector来实现多路复用,从而处理多个并发连接。

Channel的种类:

  • SocketChannel:用于基于TCP的网络通信,可以与服务器或客户端进行连接。

  • ServerSocketChannel:用于监听TCP连接,类似于传统I/O中的ServerSocket

  • DatagramChannel:用于基于UDP的网络通信。

  • FileChannel:用于从文件中读取或向文件中写入数据。

什么是 Selector?

Selector 是Java NIO(New I/O)中用于实现I/O多路复用的组件,它可以通过一个单独的线程同时监视多个通道(Channel)的事件。

Selector的作用:

  • 管理多个Channel:通过一个selector 实例,程序可以同时监听多个通道的I/O事件(如可读、可写、连接就绪等),从而使一个线程管理多个连接变得高效。

  • 非阻塞I/O:selector 通常与非阻塞通道(如 socketchannel )配合使用,实现高效的非阻塞I/O操作。它使得程序无需为每个连接创建一个线程,减少了线程的开销。

Selector的事件类型:

  • OP_READ:表示通道中有数据可读。

  • OP_WRITE:表示通道可以向其中写入数据,

  • OP_CONNECT:表示通道完成连接操作。

  • OP_ACCEPT:表示通道可以接受新的连接。

什么是缓冲区?在网络编程中,为什么使用缓冲区是重要的?

缓冲区就像是一个临时存储区,类似于你在做作业时用来放草稿的纸。在计算机领域,缓冲区是一块内存区域,用来临时存储数据,等待处理或传输。就像你先把想法写在草稿纸上,然后再把它们整理好写在最终的纸上一样。

在网络编程中,缓冲区的作用也类似。当计算机之间进行数据传输,比如发送网页、图片或文件,数据往往不是一次性就能传输完的。而且,网络传输可能会受到延迟或不稳定的影响。这时候,就像使用草稿纸一样,我们使用缓冲区来暂时存储数据,然后逐步地将数据发送或接收。

缓冲区的重要性在于它能够带来很多好处:

  • 性能优化: 缓冲区允许将数据分批次传输,减少了频繁的数据传输操作,提高了效率。

  • 数据处理: 缓冲区允许程序一次性处理大块数据,而不需要等待每个小数据块的到达。这对于复杂的数据操作非常有用。

  • 网络延迟: 缓冲区可以处理网络传输中的延迟问题。数据先被放入缓冲区,然后在适当的时机一起发送,从而减少了由于网络延迟引起的等待时间。

  • 稳定性: 缓冲区还有助于防止数据丢失。如果数据一下子全部发送,中间出了问题,可能会导致数据丢失。但是使用缓冲区,数据被分成小块,即使一部分数据丢失,其他部分仍然可以正常传输。

NIO是如何实现零拷贝技术的?

  1. MappedByteBuffer 的内存映射

MappedByteBuffer 是 NIO 基于内存映射(mmap)这种零拷贝方式的提供的一种实现,可以减少一次数据拷贝的过程。它继承自 ByteBuffer。FileChannel 定义了一个 map() 方法,它可以把一个文件从 position 位置开始的 size 大小的区域映射为内存映像文件。抽象方法 map() 方法在 FileChannel 中的定义如下:

  1. FileChannel 的 transferTo()transferFrom() 方法
  • FileChannel 的这两个方法可以在两个通道之间直接传输数据,避免数据从内核空间拷贝到用户空间再回到内核空间。

  • 底层利用了操作系统的 sendfile 系统调用。

  1. SocketChannel 和零拷贝
  • 在网络传输中,SocketChannel 结合 FileChannel.transferTo() 方法,也可以实现高效的数据传输。

  • 比如从文件中读取数据并通过网络直接发送到客户端,底层也是利用了操作系统的 sendfile 系统调用。

以上2和3主要使用了DirectByteBufDirectByteBuf 能够直接与底层操作系统的 I/O 机制进行交互;DirectByteBuf 使用直接内存分配,不需要将数据拷贝到 Java 堆中。直接内存是通过 JNI(Java Native Interface)调用系统底层分配的,操作系统会直接将内存区域映射到物理内存。