NIO优点
传统流I/O是基于字节的,所有I/O都被视为单个字节的移动;而NIO是基于块的,NIO的性能肯定优于流I/O。其性能的提高要得益于其使用的结构更接近操作系统执行I/O的方式:通道和缓冲器。我们可以把它想象成一个煤矿,通道是一个包含煤层(数据)的矿藏,而缓冲器则是派送 到矿藏的卡车。卡车载满煤炭而归,我们再从卡车上获得煤炭。也就是说,我们并没有直接和通道交互;我们只是和缓冲器交互,并把缓冲器派送到通道。通道要么从缓冲器获得数据,要么向缓冲器发送数据。
I/O的终极目标是效率,而效率离不开底层操作系统和文件系统的特性支持。这些特性包括:文件锁定、非阻塞I/O、就绪性选择、和内存映射。当今操作系统大都支持这些特性,而Java传统I/O机制并没有模拟这些通用的I/O服务。
NIO 的创建目的是为了让 Java 程序员可以实现高速 I/O 而无需编写自定义的本机代码。NIO 将最耗时的 I/O 操作(即填充和提取缓冲区)转移回操作系统,因而可以极大地提高速度。
缓冲区操作
缓冲区操作是所有I/O的基础,进程执行I/O操作,归结起来就是向操作系统发出请求,让它要么把缓冲区里的数据排干(写),要么把缓冲区填满(读)。如下图
进程使用read()
系统调用,要求其缓冲区被填满。内核随即向磁盘控制硬件发出命令,要求其从磁盘读取数据。磁盘控制器把数据直接写入内核内存缓冲区,这一步通过 DMA 完成,无需主CPU协助。一旦磁盘控制器把缓冲区装满,内核即把数据从内核空间的临时缓冲区拷贝到进程执行read()
调用时指定的缓冲区。
JAVA NIO
JAVA的NIO是基于IO多路复用模型,在不同平台上有不同的实现方式。Linux下面用的是poll和epoll,在BSD上用kqueue,在Windows上是重叠I/O。
1. 缓冲区
通道是对原 I/O 包中的流的模拟。到任何目的地(或来自任何地方)的所有数据都必须通过一个 Channel 对象。一个 Buffer 实质上是一个容器对象。发送给一个通道的所有对象都必须首先放到缓冲区中;同样地,从通道中读取的任何数据都要读到缓冲区中。
Buffer是一个对象, 它包含一些要写入或者刚读出的数据。 在 NIO 中加入 Buffer 对象,体现了新库与原 I/O 的一个重要区别。在面向流的 I/O 中,您将数据直接写入或者将数据直接读到 Stream 对象中; 在 NIO 库中,所有数据都是用缓冲区处理的。在读取数据时,它是直接读到缓冲区中的。在写入数据时,它是写入到缓冲区中的。任何时候访问 NIO 中的数据,都是将它放到缓冲区中。
缓冲区实质上就是一个数组,但它不仅仅是一个数组,缓冲区还提供了对数据的结构化访问,而且还可以跟踪系统的读/写进程。
缓冲区类型:
|
|
1.1 缓冲区基础
所有的缓冲区都具有四个属性来提供关于其所包含的数据元素的信息。
- 容量(Capacity)
缓冲区能够容纳的数据元素的最大数量。这一容量在缓冲区创建时被设定,并且永远不能
被改变。limit 决不能大于 capacity。
- 上界(Limit)
缓冲区的第一个不能被读或写的元素limit 变量表明还有多少数据需要取出(在从缓冲区写入通道时),或者还有多少空间可以放入数据(在从通道读入缓冲区时)。
position 总是小于或者等于 limit。或者说,缓冲区中现存元素的计数。
- 位置(Position)
下一个要被读或写的元素的索引。位置会自动由相应的get()和put( )方法更新。
您可以回想一下,缓冲区实际上就是美化了的数组。在从通道读取时,您将所读取的数据放到底层的数组中。 position 变量跟踪已经写了多少数据。更准确地说,它指定了下一个字节将放到数组的哪一个元素中。因此,如果您从通道中读三个字节到缓冲区中,那么缓冲区的 position 将会设置为3,指向数组中第四个元素。
同样,在写入通道时,您是从缓冲区中获取数据。 position 值跟踪从缓冲区中获取了多少数据。更准确地说,它指定下一个字节来自数组的哪一个元素。因此如果从缓冲区写了5个字节到通道中,那么缓冲区的 position 将被设置为5,指向数组的第六个元素。
- 标记(Mark)
一个备忘位置。调用mark( )来设定mark= postion。调用reset( )设定position=
mark。标记在设定前是未定义的(undefined)。
这四个属性之间总是遵循以下关系:0 <= mark <= position <= limit <= capacity
1.2 缓冲区API
|
|
上面所列出的的Buffer API并没有包括get()或put()方法。每一个Buffer类都有这两个方法,但它们所采用的参数类型,以及它们返回的数据类型,对每个子类来说都是唯一的,所以它们不能在顶层Buffer类中被抽象地声明。
1.3 字节缓冲区
字节是操作系统及其I/O设备使用的基本数据类型。当在JVM和操作系统间传递数据时,将其他的数据类型拆分成构成它们的字节是十分必要的。系统层次的I/O面向字节的性质可以在整个缓冲区的设计以及它们互相配合的服务中感受到。
|
|
- 字节顺序
多字节数值被存储在内存中的方式一般被称为endian-ness(字节顺序)。如果数字数值的最高字节——big end(大端),位于低位地址,那么系统就是大端字节顺序。字节顺序很少由软件设计者决定;它通常取决于硬件设计。如果最低字节最先保存在内存中,那么小端字节顺序。
当Internet的设计者为互联各种类型的计算机而设计网际协议(IP)时,他们意识到了在具有不同内部字节顺序的系统间传递数值数据的问题。因此,IP协议规定了使用大端的网络字节顺序概念。所有在IP分组报文的协议部分中使用的多字节数值必须先在本地主机字节顺序和通用的网络字节顺序之间进行转换。
在java.nio中,字节顺序由ByteOrder类封装。
|
|
ByteBuffer类有所不同:默认字节顺序总是ByteBuffer.BIG_ENDIAN,无论系统的固有字节顺序是什么。Java的默认字节顺序是大端字节顺序,这允许类文件等以及串行化的对象可以在任何JVM中工作。如果固有硬件字节顺序是小端,这会有性能隐患。在使用固有硬件字节顺序时,将ByteBuffer的内容当作其他数据类型存取很可能高效得多。
2. 通道
通道(Channel)是java.nio的第二个主要创新。它们既不是一个扩展也不是一项增强,而是全新、极好的Java I/O示例,提供与I/O服务的直接连接。Channel用于在字节缓冲区和位于通道另一侧的实体(通常是一个文件或套接字)之间有效地传输数据。
2.1 通道基础
|
|
通道接口允许您以一种受控且可移植的方式来访问底层的I/O服务。
通道是访问I/O服务的导管。I/O可以分为广义的两大类别:File I/O和Stream I/O。那么相应地有两种类型的通道也就不足为怪了,它们是文件(file)通道和套接字(socket)通道 ———— 一个FileChannel类和三个socket通道类:SocketChannel、ServerSocketChannel和 DatagramChannel。
2.2 通道API
ByteChannel接口,它同时继承了ReadableByteChannel 和WritableByteChannel两个接口。ByteChannel接口本身并不定义新的API方法,它是一个聚集了所继承的多个接口,并重新命名的便捷接口。根据定义,实现ByteChannel接口的通道同时也会实现ReadableByteChannel 和WritableByteChannel两个接口,所以此类通道是双向的。
通道可以以阻塞(blocking)或非阻塞(non-blocking)模式运行。非阻塞模式的通道永远不会让调用的线程休眠。请求的操作要么立即完成,要么返回一个结果表明未进行任何操作。只有面向流的(stream-oriented)的通道,如sockets和pipes才能使用非阻塞模式。
socket通道类继承了SelectableChannel。继承SelectableChannel的类可以和Selector一起使用,后者支持就绪选择。将非阻塞I/O和选择器组合起来可以使您的程序利用多路复用I/O。
与缓冲区不同,通道不能被重复使用。一个打开的通道即代表与一个特定I/O服务的特定连接并封装该连接的状态。当通道关闭时,那个连接会丢失,然后通道将不再连接任何东西。
调用通道的close( )方法时,可能会导致在通道关闭底层I/O服务的过程中线程暂时阻塞,哪怕该通道处于非阻塞模式。通道关闭时的阻塞行为(如果有的话)是高度取决于操作系统或者文件系统的。在一个通道上多次调用close( )方法是没有坏处的,但是如果第一个线程在close( )方法中阻塞,那么在它完成关闭通道之前,任何其他调用close( )方法都会阻塞。后续在该已关闭的通道上调用close( )不会产生任何操作,只会立即返回。
2.3 文件通道API
FileChannel类可以实现常用的read,write操作.同时它也提供了很多专用于文件的新方法。这些方法中的许多都是我们所熟悉的文件操作。
|
|