02.25 高级程序员应该理解的Java NIO设计理念和模型


高级程序员应该理解的Java NIO设计理念和模型


前言

前面我简单说了一下Java I/O的内容,还是有很多小伙伴反应有些内容还是理解的不是很清晰,特别是关于Java IO的流以及NIO中的缓冲区,通道和选择器等,它们到底是怎样的关系。

在这篇文章中我就对Java的这两个版本的输入输出支持设计说一下我的理解,希望对各位正在学习的小伙伴有所帮助。

因为关于技术的具体实现细节可以查看相关的技术文档有具体的说明,但是我们发现小伙伴们在研究技术文档时容易迷失在技术细节里,造成只见树木不见森林,用了好久的技术还说不出个所以然来。所以,我还是继续以我的理解思路来讲,技术脉络和联系,不谈细节。

Java对操作系统I/O的支持类型

我们知道所有应用程序的运行都是操作系统上,用Java语言开发应用程序时,通过JVM进程调用操作系统的内容处理句柄来跟操作系统通信。

我们的应用程序需要跟操作系统进行I/O操作一般有文件系统,网络数据流,内存等三类。

第一个是文件系统,我们知道操作系统都有一个文件系统组件,来管理数据的存储。

当然由于操作系统的不同,文件系统的格式有所不同。比如Unix/Linux等只能挂载一个文件系统,而Windows操作系统则是通过不同的盘驱来挂载不同的文件系统。


高级程序员应该理解的Java NIO设计理念和模型


文件系统一般都是有一个或者多个根目录,以根目录开始的路径我们称之为绝对路径,以某个路径为基础的路径表示我们称之为相对路径。

当然这里Unix/Linux操作系统只有一个根目录,而Windows操作系统由于可以有多个盘驱,所以可以挂载多个根目录。

除了文件系统,我们还有一个重要的数据来源,网络,操作系统通过网卡设备和其驱动程序来管理网络数据流。因为是基于硬件上的网络数据流,所以是二进制数据。

如此我们的操作系统会通过跟网卡交互来拷贝这些数据到操作系统管理的系统内存空间,然后再拷贝到具体的应用程序内存空间而跟应用程序进行交换数据。

所以,涉及到网络的数据,我们在Java中一般使用抽象网卡的IP地址和端口组合类,并定义了协议解析套接字定义类来处理。

至于直接跟操作系统的内存交互,内存空间的形式就是字节数组,我们可以根据数组的索引来对内存空间进行交互。为此Java语言对其进行了抽象定义。

将对底层操作系统的交互接口封装成相关的流类,来负责跟操作系统进行交互。

我们知道文件系统是基于磁盘的固定存储块的来进行抽象和算法设计的。也就是说我们的应用程序跟文件系统的交互是基于块的数据流。

由于我们应用程序需要的数据类型定义跟文件存储块的大小设计之间存在差异,所以需要操作系统控制的内存空间来作为中介将文件系统存储数据块跟应用程序数据之间错配进行调整,从而能够使得两者之间进行交互,包括数据读取和写入等。


高级程序员应该理解的Java NIO设计理念和模型


而对于需要跟网络数据进行交互的,我们知道网络数据是二进制数据,而且来自网络的数据有可能是不稳定的连续二进制数据流。

为此我们必须有一个操作系统管理的内存缓冲空间,来从网卡上接收数据,从而将基于流的数据根据约定的传输协议来解析为应用程序需要的数据类型。

这个过程由于网络的不稳定,以及其他原因,所以这种基于流的数据输入输出速度要比上面我们说的基于块的数据输入输出速度要慢的多。

经典Java I/O

对于跟字节数组的交互通信涉及到的输入输出非常简单就是其实就是对底层数组的操作,它一般用于应用程序内部线程间的数据输入输出。Java为此定义了ByteArrayInputStream和ByteArrayOutputStream两个子类流。

它们同样提供调用底层数据读取和写入指令接口,只是每次操作的是字节数组。

另外,对于数据流来说,我们知道一般通过InputStream流形式从外部数据来读取数据到应用程序,而使用OutputStream流形式来将应用程序数据写入到目标插槽中。

同样,我们可以将多个流链接起来,形成各种数据的管线方式。Java为此定义了PipedInputStream和PipedOutputStream等类型来提供连接数据流建立数据流管线能力。


高级程序员应该理解的Java NIO设计理念和模型


Java对于流的定义是一个任意长度的有序字节序列。

最初,我们的操作系统对于数据流的操作是每次只能够操作一个字节。也就是说我们的数据流应该是按照字节来进行读入和写出操作。

但是我们编写应用程序所需要定义的数据却是各种长度类型的,所以就需要将这些字节数据根据一定的规则转换为各种类型数据。这个过程需要操作系统借助其管理的内存空间来完成。

当然我们会在字节数据流的基础上,为其设定一个存储字节数组,然后根据数组长度来对数据进行缓冲,然后一起处理。

如此我们就不用再每次只操作一个字节,而是操作指定缓冲长度的字节,这体现为我们定义的一些基础类型数据流。而这些都是建立在Stream增加过滤功能设计基础上的。

我在前面的文章中说到要学好Java对于输入/输出数据操作的支持定义,必须掌握一种设计模式,那就是装饰器模式。

在这种模式下,我们会在将一些基础的核心操作抽象为统一的接口或抽象类,在Java IO中,这个基础核心就是InputStream和OutputStream,在这两个接口中我们定义了对于一个内存空间的基础操作可以调用的内容。

但是它的成员映射的都是对于底层操作系统的基础操作指令调用。

为了满足我们应用程序开发的需要,我们需要对这些基础核心操作接口进一步包装,让其能够提高我们应用程序需要的数据流操作。

为了实现这一点,我们就定义一些抽象类来实现这一接口,同时定义一些扩展的功能接口,并在实现这些扩展接口时能够利用核心基础操作的调用。


高级程序员应该理解的Java NIO设计理念和模型


如此我们就可以在具体实现这些抽象类或者接口时获得各种具体的类型定义,但是都共同实现基础的核心功能。

同时我们还能够在各个实现子类中去添加一些对基础核心操作的封装转换方法,这就是装饰模式的目的。

而整个Java IO内容都是基于基本的流操作,然后增加了扩展方法接口来实现我们应用程序需要的具体数据流创建。

Java NIO设计原理

在了解了传统的Java 对于IO的支持定义后,我们再来看一下在新一代IO支持中,对于流概念的进一步封装,为了借用现代计算机多处理器或者多内核处理器的优点,同时克服传统IO需要我们CPU来处理每个字节的接收,如果没有数据输入则会被阻塞等待,直到输入输出数据完成才能开始其它任务。

我们将对于Stream流的操作和操作目标封装到一个进程中,同时基于操作系统的特点而设计一个独立的进程来对这些工作进程进行监控,同时将大部分的IO操作交由现代计算机系统的输入输出设备独立完成。

为此Java在其NIO中抽象出了新的概念:缓冲区和管道。缓冲区相当于一个在应用程序管理的内存中定义一定缓冲空间,用于应用程序操作数据使用。而管道Channel则是封装并优化了对这些内存空间的具体操作并将指令接口暴露出来,让我们不用在去考虑流的概念,而是使用通道的概念。

由于我们的现代计算机结构中,输入输出设备具备自己的控制器,完全可以脱离CPU而独立的进行主内存和外部设备之间的数据交互操作。

由此我们开编写应用程序时,只需要向操作系统提交对应的读写指令,而具体的操作可以由操作系统转交给输入输出设备控制器通过DMA技术完成对数据的操作。


高级程序员应该理解的Java NIO设计理念和模型


而我们的CPU可以不用被阻塞等待完成,去完成其他任务。直到我们的输入输出设备触发完成或者异常事件中断,再由CPU进行相应的处理。

由一个独立的监控线程来监控各个通道是否准备好读入或者写出数据并触发相应的操作,如此我们就可以充分利用操作系统的多路复用特性,这里在设计时,为了在我们应用程序中提供对应的数据和接口,Java定义了就绪选择机制实现的Seletor概念。

它封装的需要的数据和相关操作定义,以此让我们的应用程序的操作跟底层操作系统的关于输入输出数据操的各类事件对应。

总结

综上,我大概的串讲了一下Java对于输入输出功能的编程支持。其实要理解Java的IO和NIO等设计,需要对计算机硬件和操作系统对于输入输出功能的原理有一定的了解才可以。

这些原理会告诉你,我们的应用程序其实就是对各种不同数据流的组合和处理来完成业务功能的实现的。

在经典的IO时代,我们对流Stream的不同组合处理数据反映业务逻辑的设计,而到了Java NIO时代我们借助内存缓冲区,通道和就续选择器来抽象操作对象。

我们在经典IO时代,通过不同的流管线组合来处理复杂的业务,而到了NIO时代,我们通过组合不同的通道Channel来对应用程序的复制业务逻辑进行分拆和数据处理。

在处理数据时我们应该首先区分我们操作的数据格式是以块为单位读取的文件类数据流还是以单字节为单位读取的网络数据流。其实这两类在我们应用程序编写过程中用到的都不少。

特别是操作系统的内存分页管理,让我们对于文件映射这些技术来提高块数据的读取效率非常有用。

而对于网络数据流的操作,充分利用缓冲区和多路复用技术,可以大大提高网络数据流的处理性能。


分享到:


相關文章: