本文主要讲述下自己对IO的理解,对IO的用法和细则可能没有顾虑到。

 

本文的理解基于以下几篇文章,他们对各自部分都讲的很细,对我理解IO提供了很大帮助。

https://www.cnblogs.com/ylspace/p/8128112.html

 该文主要讲解了Java IO的类体系以及他们各自的用处。

https://www.jianshu.com/p/03852a291c56 

https://blog.csdn.net/fcbayernmunchen/article/details/8635427#commentBox

https://blog.csdn.net/cringkong/article/details/80274148

这些文章讲解了有关Java Buffer的原理,包括allocate、allocateDirect、map等不同IO方式他们的底层知识。

https://blog.yoodb.com/yoodb/article/detail/1498

该文章浅谈了NIO、AIO的知识。

 

正文

  首先是我整理的思维导图

IO知识思维导图

IO的理解

  要理解Java IO设计得先了解系统IO,系统不允许程序直接访问硬盘,而是先将用户所需数据准备在系统缓冲区中再转移到用户内存,所以系统IO操作经过三个地方,1、硬盘 2、系统缓冲 3、用户进程空间。File就像是对硬盘的抽象,而stream/reader/writer就是对系统缓冲的抽象,而我们用的Buffer就是用户进程空间的抽象。

  IO的体系结构也是很直观的,对应字节处理和字符处理有了字节流inputstream、outputsream及字符流reader、writer。其中inputsream和reader对应输入流,outputsream、writer对应输出流。

根据接受或写出通道的不同,每个IO流类下衍生出了很多细分类用于处理不同通道。数据来源可以有如下:file、char[]、byte[]、网络流等。

装饰者模式

  在IO类中有一些类叫FilterXX及它的子类即是装饰者,比如我们常用的BufferInputSream和BufferOutStream,它可以自动帮我们建立并管理线程中的buffer。

  那么什么是装饰者模式呢?

 

装饰者

  首先搞清楚装饰者的目的是:动态、透明地将责任附加到对象上,若要扩展功能,装饰者提供比继承更有弹性的替代方案。为了跟装饰前看起来一样,它和被装饰者共同装配了的同个接口,为了透明增加功能,它持有了被装配对象(其实是接口),它的方法包括了装饰功能和唤醒被装饰者的方法。

Why Decorator?

  装饰者的目的是对类进行拓展,这跟继承的目标相似。他们特点是什么呢?

l  动态性 装饰者对象可以在程序运行阶段根据需求动态的拓展功能,那个类需要拓展功能只要将相应装饰类组合进去。而继承则需要编译阶段就准备好所有可能性将各种组合可能性编译好进行选择。这会造成类冗杂。

l  解耦性 如果是集成需要层层继承,而上层类的功能改变势必会影响下级类的功能。装饰者则不会。

l  装饰者(和继承)可以在被装饰者的行为上扩展行为,也可以完全替代被装饰者的行为.

l  装饰者会导致设计中出现许多小对象,如果过度使用,会让程序变得很复杂。

l  当只能获取类对象没有class文件时只能用装饰者模式进行加强。

 

 

NIO的理解

  NIO是一种非阻塞式的IO,要理解它我们得先解决几个概念。

系统IO阶段: 1、数据准备阶段(系统从磁盘读取) 2、将系统缓冲数据转移到用户本地内存阶段。

什么是阻塞?非阻塞?

答:阻塞非阻塞注重的是数据准备阶段线程有没有造成线程等待,如果数据准备阶段线程需一直等待则是阻塞的,否则是非阻塞。

什么是同步?异步?

答:同步异步注重的是整个IO过程有没有造成IO等待,一些非阻塞IO虽然系统准备阶段没有造成等待,但将数据从内核缓冲区转移到用户内存里还是需要等待。

五种IO模型

由于篇幅有限,不重点讲IO模型,观点基于《UNIIX网络编程卷1:套接字联网API》 第六章。

同步阻塞IO:会造成整个IO过程等待。

同步非阻塞IO:系统准备数据阶段不会造成等待。

信号式IO:系统准备阶段不会造成等待,并且系统准备完成会发送信号给线程。

IO复用:由一个线程管理多个IO,底层调用时select和epoll(select需论询,epoll信号驱动),多个IO只要有一个准备好就线程就不会被阻塞,有效减少了系统准备阶段等待时间。

异步IO:整个IO无需等待。完成时返回成功指示。

 

  回到我们整体NIO,很明显NIO借用的是IO复用的思想。NIO常使用的三个类即是Selector、Buffer、Channel 。使用的方式就是将Channel注册到selector内,轮询到那个IO可用就在Channel和Buffer间进行读写信息。

  Selector有三种选择方法:select()、select(long timeout)、selectNow();前两种是阻塞式的,如果一个都没准备好还是会被阻塞,selectNow()是非阻塞式的,它能立即返回是否准备好的信号。

  同样继承AbstractSelectableChannel也可以通过设置configureBlocking(false)达到非阻塞的目的。我有一个疑问就是selectNow()+非阻塞Channel是否已经达到异步了呢?因为任何一个过程都没造成等待。当然具体以后再细究,因为Channel 是native代码。。。。

 

NIO中的Buffer分配方式,ByteBuffer.allocate()、ByteBuffer().allocateDirect()以及MappedByteBuffer、TransrferTo()

  普通IO方式是通过ByteBuffer.allocate(),在  Java堆上分配内存,因此系统IO过程是:硬盘→系统缓冲→用户内存空间→系统缓冲→硬盘。

  而ByteBuffer().allocateDirect()则是在native堆上分配内存,减少了native堆到Java堆中的复制,加快了速度,但也失去了Java GC的便利性。

  MappedByteBuffer:直接建立用户空间与硬盘间的映射,IO过程省略了数据复制到系统内核缓冲的步骤。这里有个问题是为什么需要系统内核缓冲?因为我们IO时往往会继续读取局部上下文,系统为了提高IO效率,直接在内核建立缓冲区缓冲IO数据的上下文以便接下来使用,但如果我们要IO大文件,文件大小超过系统缓冲,这样缓冲区显然是无意义的,因此有了这种方法来解决大文件的IO。

  TransferTO() 它是以上方法的包装,如果传递对象是FileChannel,则使用MappedByteBuffer的方式,如果是其他channel则使用allocateDirect()方式。

 

AIO

  AIO是完全的异步IO,它实现的核心是异步通道AsynchronizedXXChannel和获取异步结果的接口Future。

  异步通道进行读写时会另开一个线程进行IO操作,操作完成它会将改变接受结果类的完成信号并且将结果赋予到接收结果类(Future)上,我们需要结果时,通过访问接收类信号量,是则获取结果。