IO-NIO 你真的理解IO吗?

我们知道IO一般有两种用途,一种是磁盘读写,一种是网络socket传输。

下图是IO的体系

此图很明显的看出,IO设计存在对称性。即 Reader和Writer对称,InputStream和OutputStream对称。

很重的一点,面试经常问道。使用了两个设计模式,即装饰模式适配器模式

装饰器模式


1.装饰器模式定义:装饰模式指的是在不必改变原类文件和使用继承的情况下,动态地扩展一个对象的功能。它是通过创建一个包装对象,也就是装饰来包裹真实的对象。

其中Writer,Reader,InputStream,OutputStream等都是抽象类。 由于java I/O库需要很多性能的各种组合,如果这些性能都是用继承来实现,那么每一种组合都需要一个类,这样就会造成大量行重复的类出现。如果采用装饰模式,那么类的数目就会大大减少,性能的重复也可以减至最少。因此装饰模式是java I/O库基本模式。装饰模式的引进,造成灵活性和复杂性的提高。因此在使用 java I/O 库时,必须理解java I/O库是由一些基本的原始流处理器和围绕它们的装饰流处理器所组成的。

那么问题来了,什么叫做流处理器呢?什么叫原始流处理器?什么叫链接流处理器呢?

为了便于理解,首先将原始流和链接流名称介绍一下,以InputStream为例。

扫描二维码关注公众号,回复: 3077211 查看本文章

原始流处理器      

  原始流处理器接收一个Byte数组对象、String对象、FileDescriptor对象或者不同类型的流源对象(就是前面所说的原始流源),并生成一个InputStream类型的流对象。在InputStream类型的流处理器中,原始流处理器包括以下四种:
    (1)ByteArrayInputStream:为多线程的通讯提供缓冲区操作工作,接受一个Byte数组作为流的源。
    (2)FileInputStream:建立一个与文件有关的输入流。接受一个File对象作为流的源。
  (3)PipedInputStream:可以和PipedOutputStream配合使用,用于读入一个数据管道的数据。接受一个PipedOutputStream作为源。
    (4)StringBufferInputStream
(已经弃用,处理时转化为byte数组用ByteArrayInputStream处理):将一个字符串缓冲区抓换为一个输入流。接受一个String对象作为流的源。
      与原始流处理器相对应的是链接流处理器。


链接流处理器       


  所谓链接流处理器就是可以接受另一个(同种类的)流对象(就是链接流源)作为流源,并对之进行功能扩展的类。InputStream类型的链接流处理器包括以下几种,它们接受另一个InputStream对象作为流源。
     (1)FilterInputStream称为过滤输入流,它将另一个输入流作为流源。这个类的子类包括以下几种:
          BufferInputStream:用来从硬盘将数据读入到一个内存缓冲区中,并从此缓冲区提供数据。
          DateInputStream:提供基于多字节的读取方法,可以读取原始数据类型的数据。
          LineNumberInputStream:提供带有行计算功能的过滤输入流。       
          PushbackInputStream: 提供特殊的功能,可以将已读取的直接“推回”输入流中。
     (2)ObjectInputStream 可以将使用ObjectInputStream串行化的原始数据类型和对象重新并行化。
     (3)SequenceInputStream可以将两个已有的输入流连接起来,形成一个输入流,从而将多个输入流排列构成一个输入流序列。
     必须注意的是,虽然PipedInuptStream接受一个流对象PipedOutputStream作为流的源,但是PipedOutputStream流对象的类型不是InputStream,因此PipedInputStream流处理器仍属于原始流处理器。 

下图很好的帮助理解原始流处理器和链接流处理器。



左边的那根管子就是我们说的原始流处理器,从硬盘中读取数据源,同时转换为相应数据类型的数据,读取到内存中。

但是问题是往往我们需要的数据是结构化的,并不是简单的数据类型。这时就需要在接上一根管子,将接收到的byte转化为int,char,short等类型。这第二根管子就叫做链接流处理器。

理解了这些,现在可以明确装饰器模式中的各个角色了。

 在所有InputStream类型的链接流处理其中,使用频率最大的就是FilterInputStream类,以这个类为抽象装饰角色的装饰模式结构非常明显和典型。以这个类为核心说明装饰模式的各个角色是由哪些流处理器扮演:
     抽象构件(Component)角色:由InputStream扮演。这是一个抽象类,为各种子类型处理器提供统一的接口。
    具体构建(Concrete Component)角色:由ByteArrayInputStream、FileInputStream、PipedInputStream以及StringBufferInputStream等原始流处理器扮演。它们实现了抽象构建角色所规定的接口,可以被链接流处理器所装饰。
     抽象装饰(Decorator)角色:由FilterInputStream扮演。它实现了InputStream所规定的接口。

     具体装饰(Concrete Decorator)角色:由几个类扮演,分别是DateInputStream、BufferedInputStream 以及两个不常用到的类LineNumberInputStream(已经弃用)和PushbackInputStream。


适配器模式

适配器模式:适配器模式(Adapter Pattern)是作为两个不兼容的接口之间的桥梁。这种类型的设计模式属于结构型模式,它结合了两个独立接口的功能。这种模式涉及到一个单一的类,该类负责加入独立的或不兼容的接口功能。举个真实的例子,读卡器是作为内存卡和笔记本之间的适配器。您将内存卡插入读卡器,再将读卡器插入笔记本,这样就可以通过笔记本来读取内存卡。

在JAVA的IO设计中,大量使用了适配器模式。首先,字节流和字符流的转换是很明显的适配器模式


转换的代码

BufferedReader reader = new BufferedReader(new InputStreamReader(new FileInputStream(new File("text.txt"))));  

应用层就介绍到这里,现在考虑底层原理,思考一个问题,JVM实际上是一个进程,是如何将硬盘中的数据读取到内存空间中呢?下面放一张原理图



DMA在我上一篇博文介绍了。解释一下这个流程

1.用户创建一个buffer缓冲区。

2.内核给磁盘控制器发命令说:我要读磁盘上的某某块磁盘块上的数据

3.在DMA的控制下,把磁盘上的数据读入到内核缓冲区

4.内核把数据从内核缓冲区复制到用户缓冲区

这时会存在一个问题,如果内核在从数据缓冲区读取数据时数据异常,也就是read()不可用的是时候。process将会被挂起,并需要等待内核从磁盘上把数据取到内核缓冲区中。

那我们可能会说:DMA为什么不直接将磁盘上的数据读入到用户缓冲区呢?一方面是 ⓑ中提到的内核缓冲区作为一个中间缓冲区。用来“适配”用户缓冲区的“任意大小”和每次读磁盘块的固定大小。另一方面则是,用户缓冲区位于用户态空间,而DMA读取数据这种操作涉及到底层的硬件,硬件一般是不能直接访问用户态空间的(OS的原因吧)

综上,由于DMA不能直接访问用户空间(用户缓冲区),普通IO操作需要将数据来回地在 用户缓冲区 和 内核缓冲区移动,这在一定程序上影响了IO的速度。那有没有相应的解决方案呢?

内核缓冲区:对于读操作而言,内核缓冲区就相当于一个“readahead cache”,当用户程序一次只需要读一小部分数据时,首先操作系统从磁盘上读一大块数据到内核缓冲区,用户程序只取走了一小部分( 我可以只 new 了一个 128B的byte数组啊! new byte[128])。当用户程序下一次再读数据,就可以直接从内核缓冲区中取了,操作系统就不需要再次访问磁盘啦!因为用户要读的数据已经在内核缓冲区啦!这也是前面提到的:为什么后续的读操作(read()方法调用)要明显地比第一次快的原因。从这个角度而言,内核缓冲区确实提高了读操作的性能。

再来看写操作:可以做到 “异步写”(write asynchronously)。也即:wirte(dest[]) 时,用户程序告诉操作系统,把dest[]数组中的内容写到XX文件中去,于是write方法就返回了。操作系统则在后台默默地把用户缓冲区中的内容(dest[])拷贝到内核缓冲区,再把内核缓冲区中的数据写入磁盘。那么,只要内核缓冲区未满,用户的write操作就可以很快地返回。这应该就是异步刷盘策略吧。


那就是直接内存映射IO,也即JAVA NIO中提到的内存映射文件,或者说 直接内存....总之,它们表达的意思都差不多。示例图如下:


从上图可以看出:内核空间的 buffer 与 用户空间的 buffer 都映射到同一块 物理内存区域。

它的主要特点如下:

①对文件的操作不需要再发read 或者 write 系统调用了-

②当用户进程访问“内存映射文件”地址时,自动产生缺页错误,然后由底层的OS负责将磁盘上的数据送到内存。

这就是是JAVA NIO中提到的内存映射缓冲区(Memory-Mapped-Buffer)它类似于JAVA NIO中的直接缓冲区(Directed Buffer)。MemoryMappedBuffer可以通过java.nio.channels.FileChannel.java(通道)的 map方法创建。

使用内存映射缓冲区来操作文件,它比普通的IO操作读文件要快得多。甚至比使用文件通道(FileChannel)操作文件 还要快。因为,使用内存映射缓冲区操作文件时,没有显示的系统调用(read,write),而且OS还会自动缓存一些文件页(memory page)。

是不是看到这里晕晕的,我也晕,哈哈。不要急,看完下面这个实例就会对IO有一个深入的理解


这是一个经典的web服务器(比如文件服务器)干的活:从磁盘中中读文件,并把文件通过网络(socket)发送给Client。

看起来很简单吧,大体上分为两步操作,第一步从磁盘中读取数据到缓存中,第二部将缓存中的数据通过socket发送过去。

但是中间一共发生了四次上下文的切换(用户态与内核态之间的切换) 和 四次拷贝操作才能完成。

①第一次上下文切换发生在 read()方法执行,表示服务器要去磁盘上读文件了,这会导致一个 sys_read()的系统调用。此时由用户态切换到内核态,完成的动作是:DMA把磁盘上的数据读入到内核缓冲区中(这也是第一次拷贝)。

②第二次上下文切换发生在read()方法的返回(这也说明read()是一个阻塞调用),表示数据已经成功从磁盘上读到内核缓冲区了。此时,由内核态返回到用户态,完成的动作是:将内核缓冲区中的数据拷贝到用户缓冲区(这是第二次拷贝)。

③第三次上下文切换发生在 send()方法执行,表示服务器准备把数据发送出去了。此时,由用户态切换到内核态,完成的动作是:将用户缓冲区中的数据拷贝到内核缓冲区(这是第三次拷贝)

④第四次上下文切换发生在 send()方法的返回【这里的send()方法可以异步返回,所谓异步返回就是:线程执行了send()之后立即从send()返回,剩下的数据拷贝及发送就交给底层操作系统实现了】。此时,由内核态返回到用户态,完成的动作是:将内核缓冲区中的数据送到 protocol engine.(这是第四次拷贝)

是不是更晕了,那就对了,哈哈

猜你喜欢

转载自blog.csdn.net/qq_27631217/article/details/80519025