APP下载
报价宝  ›  科技  › 

史上最强Java NIO入门:担心从入门到放弃的 请读这篇

报价宝 来源:baojiabao.com 发布时间:2019-06-30 16:49:00 09月21日更新
报价宝综合消息史上最强Java NIO入门:担心从入门到放弃的 请读这篇

本文原题“《NIO 入门》,作者为“Gregory M. Travis”,他是《JDK 1.4 Tutorial》等书籍的作者。

1、引言

Java NIO是Java 1.4版加入的新特性,虽然Java技术日新月异,但历经10年,NIO依然为Java技术领域里最为重要的基础技术栈,而且依据现实的应用趋势,在可以预见的未来,它仍将继续在Java技术领域占据重要位置。

网上有关Java NIO的技术文章,虽然写的也不错,但通常是看完一篇马上懵逼。接着再看!然后,会更懵逼。。。 哈哈哈!

本文作者厚积薄发,以远比一般的技术部落格或技术作者更深厚的Java技术储备,为你由浅入深,从零讲解到底什么是Java NIO。本文即使没有多少 Java 程式设计经验的读者也能很容易地开始学习 NIO。

(本文同步释出于:http://www.52im.net/thread-2640-1-1.html)

2、关于作者

Gregory M. Travis:技术顾问、多产的技术作家,现居纽约。他从Java语言释出的第1天起,就已经是Java程序员啦!

Gregory M. Travis是《JDK 1.4 Tutorial》一书的作者,Java程序员应该都清楚,能写好JDK Tutorial这种书籍或手册的,除了SUN(现在是Oracle)公司的Java建立者们,余下的也只有各路实打实的Java大牛们才能hold住。

3、在开始之前

3.1 关于本教程

新的输入/输出 (NIO) 库是在 JDK 1.4 中引入的。NIO 弥补了原来的 I/O 的不足,它在标准 Java 程式码中提供了高速的、面向块的 I/O。通过定义包含资料的类,以及通过以块的形式处理这些资料,NIO 不用使用本机程式码就可以利用低阶优化,这是原来的 I/O 包所无法做到的。

在本教程中,我们将讨论 NIO 库的几乎所有方面,从高阶的概念性内容到底层的程式设计细节。除了学习诸如缓冲区和通道这样的关键 I/O 元素外,您还有机会看到在更新后的库中标准 I/O 是如何工作的。您还会了解只能通过 NIO 来完成的工作,如异步 I/O 和直接缓冲区。

在本教程中,我们将使用展示 NIO 库的不同方面的程式码示例。几乎每一个程式码示例都是一个大的 Java 程式的一部分,您可以在本文末的附件中下载到这个 Java 程式。在做这些练习时,我们推荐您在自己的系统上下载、编译和执行这些程式。在您学习了本教程以后,这些程式码将为您的 NIO 程式设计努力提供一个起点。

本教程是为希望学习更多关于 Java NIO 库的知识的所有程序员而写的。为了最大程度地从这里的讨论中获益,您应该理解基本的 Java 程式设计概念,如类、继承和使用包。多少熟悉一些原来的 I/O 库(来自java.io.* 包)也会有所帮助。

虽然本教程要求掌握 Java 语言的工作词汇和概念,但是不需要有很多实际程式设计经验。除了彻底介绍与本教程有关的所有概念外,我还保持程式码示例尽可能短小和简单。目的是让即使没有多少 Java 程式设计经验的读者也能容易地开始学习 NIO。

3.2 如何执行程式码

源代码归档档案(请从本文末的附件下载之)包含了本教程中使用的所有程式。每一个程式都由一个 Java 档案构成。每一个档案都根据名称来识别,并且可以容易地与它所展示的程式设计概念相关联。

教程中的一些程式需要命令列引数才能执行。要从命令列执行一个程式,只需使用最方便的命令列提示符。在 Windows 中,命令列提供符是 “Command” 或者 “command.com” 程式。在 UNIX 中,可以使用任何 shell。

需要安装 JDK 1.4 并将它包括在路径中,才能完成本教程中的练习。如果需要安装和配置 JDK 1.4 的帮助,请参见 参考资料 。

4、输入/输出:概念性描述

4.1 I/O 简介

I/O ? 或者输入/输出 ? 指的是计算机与外部世界或者一个程式与计算机的其余部分的之间的界面。它对于任何计算机系统都非常关键,因而所有 I/O 的主体实际上是内建在操作系统中的。单独的程式一般是让系统为它们完成大部分的工作。

在 Java 程式设计中,直到最近一直使用 流 的方式完成 I/O。所有 I/O 都被视为单个的字节的移动,通过一个称为 Stream 的物件一次移动一个字节。流 I/O 用于与外部世界接触。它也在内部使用,用于将物件转换为字节,然后再转换回物件。

NIO 与原来的 I/O 有同样的作用和目的,但是它使用不同的方式? 块 I/O。正如您将在本教程中学到的,块 I/O 的效率可以比流 I/O 高许多。

4.2 为什么要使用 NIO?

NIO 的建立目的是为了让 Java 程序员可以实现高速 I/O 而无需编写自定义的本机程式码。NIO 将最耗时的 I/O 操作(即填充和提取缓冲区)转移回操作系统,因而可以极大地提高速度。

4.3 流与块的比较

原来的 I/O 库(在 java.io.*中) 与 NIO 最重要的区别是资料打包和传输的方式。正如前面提到的,原来的 I/O 以流的方式处理资料,而 NIO 以块的方式处理资料。

面向流 的 I/O 系统一次一个字节地处理资料。一个输入流产生一个字节的资料,一个输出流消费一个字节的资料。为流式资料建立过滤器非常容易。连结几个过滤器,以便每个过滤器只负责单个复杂处理机制的一部分,这样也是相对简单的。不利的一面是,面向流的 I/O 通常相当慢。

一个 面向块 的 I/O 系统以块的形式处理资料。每一个操作都在一步中产生或者消费一个数据块。按块处理资料比按(流式的)字节处理资料要快得多。但是面向块的 I/O 缺少一些面向流的 I/O 所具有的优雅性和简单性。

4.4 整合的 I/O

在 JDK 1.4 中原来的 I/O 包和 NIO 已经很好地集成了。 java.io.* 已经以 NIO 为基础重新实现了,所以现在它可以利用 NIO 的一些特性。例如, java.io.* 包中的一些类包含以块的形式读写资料的方法,这使得即使在更面向流的系统中,处理速度也会更快。

也可以用 NIO 库实现标准 I/O 功能。例如,可以容易地使用块 I/O 一次一个字节地移动资料。但是正如您会看到的,NIO 还提供了原 I/O 包中所没有的许多好处

5、通道和缓冲区

5.1 概述

通道 和 缓冲区 是 NIO 中的核心物件,几乎在每一个 I/O 操作中都要使用它们。

通道是对原 I/O 包中的流的模拟。到任何目的地(或来自任何地方)的所有资料都必须通过一个 Channel 物件。一个 Buffer 实质上是一个容器物件。传送给一个通道的所有物件都必须首先放到缓冲区中;同样地,从通道中读取的任何资料都要读到缓冲区中。

在本节中,您会了解到 NIO 中通道和缓冲区是如何工作的。

5.2 什么是缓冲区?

Buffer 是一个物件, 它包含一些要写入或者刚读出的资料。 在 NIO 中加入 Buffer 物件,体现了新库与原 I/O 的一个重要区别。在面向流的 I/O 中,您将资料直接写入或者将资料直接读到 Stream 物件中。

在 NIO 库中,所有资料都是用缓冲区处理的。在读取资料时,它是直接读到缓冲区中的。在写入资料时,它是写入到缓冲区中的。任何时候访问 NIO 中的资料,您都是将它放到缓冲区中。

缓冲区实质上是一个数组。通常它是一个字节阵列,但是也可以使用其他种类的阵列。但是一个缓冲区不 仅仅 是一个数组。缓冲区提供了对资料的结构化访问,而且还可以跟踪系统的读/写程序。

5.3 缓冲区型别

最常用的缓冲区型别是 ByteBuffer。一个 ByteBuffer 可以在其底层字节阵列上进行 get/set 操作(即字节的获取和设定)。

ByteBuffer 不是 NIO 中唯一的缓冲区型别。

事实上,对于每一种基本 Java 型别都有一种缓冲区型别:

ByteBuffer

CharBuffer

ShortBuffer

IntBuffer

LongBuffer

FloatBuffer

DoubleBuffer

每一个 Buffer 类都是 Buffer 界面的一个例项。 除了 ByteBuffer,每一个 Buffer 类都有完全一样的操作,只是它们所处理的资料型别不一样。因为大多数标准 I/O 操作都使用 ByteBuffer,所以它具有所有共享的缓冲区操作以及一些特有的操作。

现在您可以花一点时间执行 UseFloatBuffer.java(请从本文末的附件下载之),它包含了型别化的缓冲区的一个应用例子。

5.4 什么是通道?

Channel是一个物件,可以通过它读取和写入资料。拿 NIO 与原来的 I/O 做个比较,通道就像是流。

正如前面提到的,所有资料都通过 Buffer 物件来处理。您永远不会将字节直接写入通道中,相反,您是将资料写入包含一个或者多个字节的缓冲区。同样,您不会直接从通道中读取字节,而是将资料从通道读入缓冲区,再从缓冲区获取这个字节。

5.5 通道型别

通道与流的不同之处在于通道是双向的。而流只是在一个方向上移动(一个流必须是 InputStream 或者 OutputStream 的子类), 而 通道 可以用于读、写或者同时用于读写。

因为它们是双向的,所以通道可以比流更好地反映底层操作系统的真实情况。特别是在 UNIX 模型中,底层操作系统通道是双向的。

6、从理论到实践:NIO 中的读和写

6.1 概述

读和写是 I/O 的基本过程。从一个通道中读取很简单:只需建立一个缓冲区,然后让通道将资料读到这个缓冲区中。写入也相当简单:建立一个缓冲区,用资料填充它,然后让通道用这些资料来执行写入操作。

在本节中,我们将学习有关在 Java 程式中读取和写入资料的一些知识。我们将回顾 NIO 的主要元件(缓冲区、通道和一些相关的方法),看看它们是如何互动以进行读写的。在接下来的几节中,我们将更详细地分析这其中的每个元件以及其互动。

6.2 从档案中读取

在我们第一个练习中,我们将从一个档案中读取一些资料。如果使用原来的 I/O,那么我们只需建立一个 FileInputStream 并从它那里读取。而在 NIO 中,情况稍有不同:我们首先从 FileInputStream 获取一个 Channel 物件,然后使用这个通道来读取资料。

在 NIO 系统中,任何时候执行一个读操作,您都是从通道中读取,但是您不是 直接 从通道读取。因为所有资料最终都驻留在缓冲区中,所以您是从通道读到缓冲区中。

因此读取档案涉及三个步骤:(1) 从 FileInputStream 获取 Channel,(2) 建立 Buffer,(3) 将资料从 Channel 读到 Buffer 中。

现在,让我们看一下这个过程。

6.3 三个容易的步骤

第一步是获取通道。我们从 FileInputStream 获取通道:

FileInputStream fin = newFileInputStream( "readandshow.txt");

FileChannel fc = fin.getChannel();

下一步是建立缓冲区:

ByteBuffer buffer = ByteBuffer.allocate( 1024);

最后,需要将资料从通道读到缓冲区中,如下所示:

fc.read( buffer );

您会注意到,我们不需要告诉通道要读 多少资料 到缓冲区中。每一个缓冲区都有复杂的内部统计机制,它会跟踪已经读了多少资料以及还有多少空间可以容纳更多的资料。我们将在 缓冲区内部细节 中介绍更多关于缓冲区统计机制的内容。

6.4 写入档案

在 NIO 中写入档案类似于从档案中读取。首先从 FileOutputStream 获取一个通道:

FileOutputStream fout = newFileOutputStream( "writesomebytes.txt");

FileChannel fc = fout.getChannel();

下一步是建立一个缓冲区并在其中放入一些资料 - 在这里,资料将从一个名为 message 的阵列中取出,这个阵列包含字串 "Some bytes" 的 ASCII 字节(本教程后面将会解释 buffer.flip() 和 buffer.put() 呼叫)。

ByteBuffer buffer = ByteBuffer.allocate( 1024);

for(intii=0; ii buffer.put( message[ii] );

}

buffer.flip();

最后一步是写入缓冲区中:

fc.write( buffer );

注意在这里同样不需要告诉通道要写入多资料。缓冲区的内部统计机制会跟踪它包含多少资料以及还有多少资料要写入。

6.5 读写结合

下面我们将看一下在结合读和写时会有什么情况。我们以一个名为 CopyFile.java 的简单程式作为这个练习的基础,它将一个档案的所有内容拷贝到另一个档案中。CopyFile.java 执行三个基本操作:首先建立一个 Buffer,然后从原始档中将资料读到这个缓冲区中,然后将缓冲区写入目标档案。这个程式不断重复 ― 读、写、读、写 ― 直到原始档结束。

CopyFile 程式让您看到我们如何检查操作的状态,以及如何使用 clear() 和 flip() 方法重设缓冲区,并准备缓冲区以便将新读取的资料写到另一个通道中。

6.6 执行 CopyFile 例子

因为缓冲区会跟踪它自己的资料,所以 CopyFile 程式的内部循环 (inner loop) 非常简单,如下所示:

fcin.read( buffer );

fcout.write( buffer );

第一行将资料从输入通道 fcin 中读入缓冲区,第二行将这些资料写到输出通道 fcout 。

6.7 检查状态

下一步是检查拷贝何时完成。当没有更多的资料时,拷贝就算完成,并且可以在 read() 方法返回 -1 是判断这一点,如下所示:

intr = fcin.read( buffer );

if(r==-1) {

break;

}

6.8 重设缓冲区

最后,在从输入通道读入缓冲区之前,我们呼叫 clear() 方法。同样,在将缓冲区写入输出通道之前,我们呼叫 flip() 方法,如下所示:

buffer.clear();

intr = fcin.read( buffer );

if(r==-1) {

break;

}

buffer.flip();

fcout.write( buffer );

clear() 方法重设缓冲区,使它可以接受读入的资料。 flip() 方法让缓冲区可以将新读入的资料写入另一个通道。

7、缓冲区内部细节

7.1 概述

本节将介绍 NIO 中两个重要的缓冲区元件:状态变数和访问方法 (accessor)。

状态变数是前一节中提到的"内部统计机制"的关键。每一个读/写操作都会改变缓冲区的状态。通过记录和跟踪这些变化,缓冲区就可能够内部地管理自己的资源。

在从通道读取资料时,资料被放入到缓冲区。在有些情况下,可以将这个缓冲区直接写入另一个通道,但是在一般情况下,您还需要检视资料。这是使用 访问方法 get() 来完成的。同样,如果要将原始资料放入缓冲区中,就要使用访问方法 put()。

在本节中,您将学习关于 NIO 中的状态变数和访问方法的内容。我们将描述每一个元件,并让您有机会看到它的实际应用。虽然 NIO 的内部统计机制初看起来可能很复杂,但是您很快就会看到大部分的实际工作都已经替您完成了。您可能习惯于通过手工编码进行簿记 ― 即使用字节阵列和索引变数,现在它已在 NIO 中内部地处理了。

7.2 状态变数

可以用三个值指定缓冲区在任意时刻的状态:

position

limit

capacity

这三个变数一起可以跟踪缓冲区的状态和它所包含的资料。我们将在下面的小节中详细分析每一个变数,还要介绍它们如何适应典型的读/写(输入/输出)程序。在这个例子中,我们假定要将资料从一个输入通道拷贝到一个输出通道。

7.3 Position

您可以回想一下,缓冲区实际上就是美化了的阵列。在从通道读取时,您将所读取的资料放到底层的阵列中。 position 变数跟踪已经写了多少资料。更准确地说,它指定了下一个字节将放到阵列的哪一个元素中。因此,如果您从通道中读三个字节到缓冲区中,那么缓冲区的 position 将会设定为3,指向阵列中第四个元素。

同样,在写入通道时,您是从缓冲区中获取资料。 position 值跟踪从缓冲区中获取了多少资料。更准确地说,它指定下一个字节来自阵列的哪一个元素。因此如果从缓冲区写了5个字节到通道中,那么缓冲区的 position 将被设定为5,指向阵列的第六个元素。

7.4 Limit

limit 变量表明还有多少资料需要取出(在从缓冲区写入通道时),或者还有多少空间可以放入资料(在从通道读入缓冲区时)。

position 总是小于或者等于 limit。

7.5 Capacity

缓冲区的 capacity 表明可以储存在缓冲区中的最大资料容量。实际上,它指定了底层阵列的大小 ― 或者至少是指定了准许我们使用的底层阵列的容量。

limit 决不能大于 capacity。

7.6 观察变数

我们首先观察一个新建立的缓冲区。出于本例子的需要,我们假设这个缓冲区的 总容量 为8个字节。

Buffer 的状态如下所示:

回想一下 ,limit 决不能大于 capacity,此例中这两个值都被设定为 8。我们通过将它们指向阵列的尾部之后(如果有第8个槽,则是第8个槽所在的位置)来说明这点。

position 设定为0。如果我们读一些资料到缓冲区中,那么下一个读取的资料就进入 slot 0 。如果我们从缓冲区写一些资料,从缓冲区读取的下一个字节就来自 slot 0 。

position 设定如下所示:

由于 capacity 不会改变,所以我们在下面的讨论中可以忽略它。

7.7 第一次读取

现在我们可以开始在新建立的缓冲区上进行读/写操作。首先从输入通道中读一些资料到缓冲区中。第一次读取得到三个字节。它们被放到阵列中从 position 开始的位置,这时 position 被设定为 0。

读完之后,position 就增加到 3,如下所示:

limit 没有改变。

7.8 第二次读取

在第二次读取时,我们从输入通道读取另外两个字节到缓冲区中。

这两个字节储存在由 position 所指定的位置上, position 因而增加 2:

limit 没有改变。

7.9 flip

现在我们要将资料写到输出通道中。在这之前,我们必须呼叫 flip() 方法。

这个方法做两件非常重要的事:

1)它将 limit 设定为当前 position;

2)它将 position 设定为 0。

前一小节中的图显示了在 flip 之前缓冲区的情况。

下面是在 flip 之后的缓冲区:

我们现在可以将资料从缓冲区写入通道了。 position 被设定为 0,这意味着我们得到的下一个字节是第一个字节。 limit 已被设定为原来的 position,这意味着它包括以前读到的所有字节,并且一个字节也不多。

7.10 第一次写入

在第一次写入时,我们从缓冲区中取四个字节并将它们写入输出通道。

这使得 position 增加到 4,而 limit 不变,如下所示:

7.11 第二次写入

我们只剩下一个字节可写了。 limit在我们呼叫 flip() 时被设定为 5,并且 position 不能超过 limit。所以最后一次写入操作从缓冲区取出一个字节并将它写入输出通道。

这使得 position 增加到 5,并保持 limit 不变,如下所示:

7.12 clear

最后一步是呼叫缓冲区的 clear() 方法。这个方法重设缓冲区以便接收更多的字节。

Clear 做两种非常重要的事情:

1)它将 limit 设定为与 capacity 相同;

2)它设定 position 为 0。

下图显示了在呼叫 clear() 后缓冲区的状态:

缓冲区现在可以接收新的资料了。

7.13 访问方法

到目前为止,我们只是使用缓冲区将资料从一个通道转移到另一个通道。然而,程式经常需要直接处理资料。例如,您可能需要将使用者资料储存到磁盘。在这种情况下,您必须将这些资料直接放入缓冲区,然后用通道将缓冲区写入磁盘。

或者,您可能想要从磁盘读取使用者资料。在这种情况下,您要将资料从通道读到缓冲区中,然后检查缓冲区中的资料。

在本节的最后,我们将详细分析如何使用 ByteBuffer 类的 get() 和 put() 方法直接访问缓冲区中的资料。

7.14 get() 方法

ByteBuffer 类中有四个 get() 方法:

1)byte get();

2)ByteBuffer get( byte dst[] );

3)ByteBuffer get( byte dst[], int offset, int length );

4)byte get( int index );

第一个方法获取单个字节。第二和第三个方法将一组字节读到一个数组中。第四个方法从缓冲区中的特定位置获取字节。那些返回 ByteBuffer 的方法只是返回呼叫它们的缓冲区的 this 值。

此外,我们认为前三个 get() 方法是相对的,而最后一个方法是绝对的。 相对 意味着 get() 操作服从 limit 和 position 值 ― 更明确地说,字节是从当前 position 读取的,而 position 在 get 之后会增加。另一方面,一个 绝对 方法会忽略 limit 和 position 值,也不会影响它们。事实上,它完全绕过了缓冲区的统计方法。

上面列出的方法对应于 ByteBuffer 类。其他类有等价的 get() 方法,这些方法除了不是处理字节外,其它方面是是完全一样的,它们处理的是与该缓冲区类相适应的型别。

7.15 put()方法

ByteBuffer 类中有五个 put() 方法:

1)ByteBuffer put( byte b );

2)ByteBuffer put( byte src[] );

3)ByteBuffer put( byte src[], int offset, int length );

4)ByteBuffer put( ByteBuffer src );

5)ByteBuffer put( int index, byte b );

第一个方法 写入(put) 单个字节。第二和第三个方法写入来自一个数组的一组字节。第四个方法将资料从一个给定的源 ByteBuffer 写入这个 ByteBuffer。第五个方法将字节写入缓冲区中特定的 位置 。那些返回 ByteBuffer 的方法只是返回呼叫它们的缓冲区的 this 值。

与 get() 方法一样,我们将把 put() 方法划分为 相对 或者 绝对 的。前四个方法是相对的,而第五个方法是绝对的。

上面显示的方法对应于 ByteBuffer 类。其他类有等价的 put() 方法,这些方法除了不是处理字节之外,其它方面是完全一样的。它们处理的是与该缓冲区类相适应的型别。

7.16 型别化的 get() 和 put() 方法

除了前些小节中描述的 get() 和 put() 方法, ByteBuffer 还有用于读写不同型别的值的其他方法。

如下所示:

getByte()

getChar()

getShort()

getInt()

getLong()

getFloat()

getDouble()

putByte()

putChar()

putShort()

putInt()

putLong()

putFloat()

putDouble()

事实上,这其中的每个方法都有两种型别 ― 一种是相对的,另一种是绝对的。它们对于读取格式化的二进位制资料(如影象档案的头部)很有用。

您可以在例子程式 TypesInByteBuffer.java 中看到这些方法的实际应用。

7.17 缓冲区的使用:一个内部循环

下面的内部循环概括了使用缓冲区将资料从输入通道拷贝到输出通道的过程。

while(true) {

buffer.clear();

intr = fcin.read( buffer );

if(r==-1) {

break;

}

buffer.flip();

fcout.write( buffer );

}

read() 和 write() 呼叫得到了极大的简化,因为许多工作细节都由缓冲区完成了。 clear() 和 flip() 方法用于让缓冲区在读和写之间切换。

8、关于缓冲区的更多内容

8.1 概述

到目前为止,您已经学习了使用缓冲区进行日常工作所需要掌握的大部分内容。我们的例子没怎么超出标准的读/写过程种类,在原来的 I/O 中可以像在 NIO 中一样容易地实现这样的标准读写过程。

本节将讨论使用缓冲区的一些更复杂的方面,比如缓冲区分配、包装和分片。我们还会讨论 NIO 带给 Java 平台的一些新功能。您将学到如何建立不同型别的缓冲区以达到不同的目的,如可保护资料不被修改的 只读 缓冲区,和直接对映到底层操作系统缓冲区的 直接 缓冲区。我们将在本节的最后介绍如何在 NIO 中建立内存对映档案。

8.2 缓冲区分配和包装

在能够读和写之前,必须有一个缓冲区。要建立缓冲区,您必须 分配 它。我们使用静态方法 allocate() 来分配缓冲区:

1ByteBuffer buffer = ByteBuffer.allocate( 1024);

allocate() 方法分配一个具有指定大小的底层阵列,并将它包装到一个缓冲区物件中 ― 在本例中是一个 ByteBuffer。

您还可以将一个现有的阵列转换为缓冲区,如下所示:

bytearray[] = newbyte[1024];

ByteBuffer buffer = ByteBuffer.wrap( array );

本例使用了 wrap() 方法将一个数组包装为缓冲区。必须非常小心地进行这类操作。一旦完成包装,底层资料就可以通过缓冲区或者直接访问。

8.3 缓冲区分片

slice() 方法根据现有的缓冲区建立一种 子缓冲区 。也就是说,它建立一个新的缓冲区,新缓冲区与原来的缓冲区的一部分共享资料。

使用例子可以最好地说明这点。让我们首先建立一个长度为 10 的 ByteBuffer:

ByteBuffer buffer = ByteBuffer.allocate( 10);

然后使用资料来填充这个缓冲区,在第 n 个槽中放入数字 n:

for(inti=0; i buffer.put( (byte)i );

}

现在我们对这个缓冲区 分片 ,以建立一个包含槽 3 到槽 6 的子缓冲区。在某种意义上,子缓冲区就像原来的缓冲区中的一个 视窗 。

视窗的起始和结束位置通过设定 position 和 limit 值来指定,然后呼叫 Buffer 的 slice() 方法:

buffer.position( 3);

buffer.limit( 7);

ByteBuffer slice = buffer.slice();

片 是缓冲区的 子缓冲区 。不过, 片段 和 缓冲区 共享同一个底层资料阵列,我们在下一节将会看到这一点。

8.4 缓冲区份片和资料共享

我们已经建立了原缓冲区的子缓冲区,并且我们知道缓冲区和子缓冲区共享同一个底层资料阵列。让我们看看这意味着什么。

我们遍历子缓冲区,将每一个元素乘以 11 来改变它。例如,5 会变成 55。

for(inti=0; i byteb = slice.get( i );

b *= 11;

slice.put( i, b );

}

最后,再看一下原缓冲区中的内容:

buffer.position( 0);

buffer.limit( buffer.capacity() );

while(buffer.remaining()>0) {

System.out.println( buffer.get() );

}

结果表明只有在子缓冲区视窗中的元素被改变了:

$ java SliceBuffer

0

1

2

33

44

55

66

7

8

9

缓冲区片对于促进抽象非常有帮助。可以编写自己的函式处理整个缓冲区,而且如果想要将这个过程应用于子缓冲区上,您只需取主缓冲区的一个片,并将它传递给您的函式。这比编写自己的函式来取额外的引数以指定要对缓冲区的哪一部分进行操作更容易。

8.5 只读缓冲区

只读缓冲区非常简单 ― 您可以读取它们,但是不能向它们写入。可以通过呼叫缓冲区的 asReadOnlyBuffer() 方法,将任何常规缓冲区转换为只读缓冲区,这个方法返回一个与原缓冲区完全相同的缓冲区(并与其共享资料),只不过它是只读的。

只读缓冲区对于保护资料很有用。在将缓冲区传递给某个物件的方法时,您无法知道这个方法是否会修改缓冲区中的资料。建立一个只读的缓冲区可以 保证 该缓冲区不会被修改。

不能将只读的缓冲区转换为可写的缓冲区。

8.6 直接和间接缓冲区

另一种有用的 ByteBuffer 是直接缓冲区。 直接缓冲区 是为加快 I/O 速度,而以一种特殊的方式分配其内存的缓冲区。

实际上,直接缓冲区的准确定义是与实现相关的。

Sun(现在是Oracle) 的文件是这样描述直接缓冲区的:

给定一个直接字节缓冲区,Java 虚拟机器将尽最大努力直接对它执行本机 I/O 操作。也就是说,它会在每一次呼叫底层操作系统的本机 I/O 操作之前(或之后),尝试避免将缓冲区的内容拷贝到一个中间缓冲区中(或者从一个中间缓冲区中拷贝资料)。

您可以在例子程式 FastCopyFile.java(请从文末附件中下载之) 中看到直接缓冲区的实际应用,这个程式是 CopyFile.java 的另一个版本,它使用了直接缓冲区以提高速度。

还可以用内存对映档案建立直接缓冲区。

8.7 内存对映档案 I/O

内存对映档案 I/O 是一种读和写档案资料的方法,它可以比常规的基于流或者基于通道的 I/O 快得多。

内存对映档案 I/O 是通过使档案中的资料神奇般地出现为内存阵列的内容来完成的。这其初听起来似乎不过就是将整个档案读到内存中,但是事实上并不是这样。一般来说,只有档案中实际读取或者写入的部分才会送入(或者 对映 )到内存中。

内存对映并不真的神奇或者多么不寻常。现代操作系统一般根据需要将档案的部分对映为内存的部分,从而实现档案系统。Java 内存对映机制不过是在底层操作系统中可以采用这种机制时,提供了对该机制的访问。

尽管建立内存对映档案相当简单,但是向它写入可能是危险的。仅只是改变阵列的单个元素这样的简单操作,就可能会直接修改磁盘上的档案。修改资料与将资料储存到磁盘是没有分开的。

8.8 将档案对映到内存

了解内存对映的最好方法是使用例子。在下面的例子中,我们要将一个 FileChannel (它的全部或者部分)对映到内存中。为此我们将使用 FileChannel.map() 方法。

下面程式码行将档案的前 1024 个字节对映到内存中:

MappedByteBuffer mbb = fc.map( FileChannel.MapMode.READ_WRITE, 0, 1024);

map() 方法返回一个 MappedByteBuffer,它是 ByteBuffer 的子类。因此,您可以像使用其他任何 ByteBuffer 一样使用新对映的缓冲区,操作系统会在需要时负责执行行对映。

9、分散和聚集

9.1 概述

分散/聚集 I/O 是使用多个而不是单个缓冲区来储存资料的读写方法。

一个分散的读取就像一个常规通道读取,只不过它是将资料读到一个缓冲区阵列中而不是读到单个缓冲区中。同样地,一个聚集写入是向缓冲区阵列而不是向单个缓冲区写入资料。

分散/聚集 I/O 对于将资料流划分为单独的部分很有用,这有助于实现复杂的资料格式。

9.2 分散/聚集 I/O

通道可以有选择地实现两个新的界面: ScatteringByteChannel 和 GatheringByteChannel。

一个 ScatteringByteChannel 是一个具有两个附加读方法的通道:

longread( ByteBuffer[] dsts );

longread( ByteBuffer[] dsts, intoffset, intlength );

这些 long read() 方法很像标准的 read 方法,只不过它们不是取单个缓冲区而是取一个缓冲区阵列。

在 分散读取 中,通道依次填充每个缓冲区。填满一个缓冲区后,它就开始填充下一个。在某种意义上,缓冲区阵列就像一个大缓冲区。

9.3 分散/聚集的应用

分散/聚集 I/O 对于将资料划分为几个部分很有用。例如,您可能在编写一个使用讯息物件的网络应用程序,每一个讯息被划分为固定长度的头部和固定长度的正文。您可以建立一个刚好可以容纳头部的缓冲区和另一个刚好可以容难正文的缓冲区。当您将它们放入一个数组中并使用分散读取来向它们读入讯息时,头部和正文将整齐地划分到这两个缓冲区中。

我们从缓冲区所得到的方便性对于缓冲区阵列同样有效。因为每一个缓冲区都跟踪自己还可以接受多少资料,所以分散读取会自动找到有空间接受资料的第一个缓冲区。在这个缓冲区填满后,它就会移动到下一个缓冲区。

9.4 聚集写入

聚集写入 类似于分散读取,只不过是用来写入。它也有接受缓冲区阵列的方法:

longwrite( ByteBuffer[] srcs );

longwrite( ByteBuffer[] srcs, intoffset, intlength );

聚集写对于把一组单独的缓冲区中组成单个数据流很有用。为了与上面的讯息例子保持一致,您可以使用聚集写入来自动将网络讯息的各个部分组装为单个数据流,以便跨越网络传输讯息。

从例子程式 UseScatterGather.java(请从文末附件中下载之) 中可以看到分散读取和聚集写入的实际应用。

10、档案锁定

10.1 概述

档案锁定初看起来可能让人迷惑。它 似乎 指的是防止程式或者使用者访问特定档案。事实上,档案锁就像常规的 Java 物件锁 ― 它们是 劝告式的(advisory) 锁。它们不阻止任何形式的资料访问,相反,它们通过锁的共享和获取赖允许系统的不同部分相互协调。

您可以锁定整个档案或者档案的一部分。如果您获取一个排它锁,那么其他人就不能获得同一个档案或者档案的一部分上的锁。如果您获得一个共享锁,那么其他人可以获得同一个档案或者档案一部分上的共享锁,但是不能获得排它锁。档案锁定并不总是出于保护资料的目的。例如,您可能临时锁定一个档案以保证特定的写操作成为原子的,而不会有其他程式的干扰。

大多数操作系统提供了档案系统锁,但是它们并不都是采用同样的方式。有些实现提供了共享锁,而另一些仅提供了排它锁。事实上,有些实现使得档案的锁定部分不可访问,尽管大多数实现不是这样的。

在本节中,您将学习如何在 NIO 中执行简单的档案锁过程,我们还将探讨一些保证被锁定的档案尽可能可移植的方法。

10.2 锁定档案

要获取档案的一部分上的锁,您要呼叫一个开启的 FileChannel 上的 lock() 方法。注意,如果要获取一个排它锁,您必须以写方式开启档案。

RandomAccessFile raf = newRandomAccessFile( "usefilelocks.txt", "rw");

FileChannel fc = raf.getChannel();

FileLock lock = fc.lock( start, end, false);

在拥有锁之后,您可以执行需要的任何敏感操作,然后再释放锁:

lock.release();

在释放锁后,尝试获得锁的其他任何程式都有机会获得它。

本小节的例子程式 UseFileLocks.java 必须与它自己并行执行。这个程式获取一个档案上的锁,持有三秒钟,然后释放它。如果同时执行这个程式的多个例项,您会看到每个例项依次获得锁。

10.3 档案锁定和可移植性

档案锁定可能是一个复杂的操作,特别是考虑到不同的操作系统是以不同的方式实现锁这一事实。

下面的指导原则将帮助您尽可能保持程式码的可移植性:

1)只使用排它锁;

2)将所有的锁视为劝告式的(advisory)。

11、连网和异步 I/O

11.1 概述

连网是学习异步 I/O 的很好基础,而异步 I/O 对于在 Java 语言中执行任何输入/输出过程的人来说,无疑都是必须具备的知识。NIO 中的连网与 NIO 中的其他任何操作没有什么不同 ― 它依赖通道和缓冲区,而您通常使用 InputStream 和 OutputStream 来获得通道。

本节首先介绍异步 I/O 的基础 ― 它是什么以及它不是什么,然后转向更实用的、程式性的例子。

11.2 异步 I/O

异步 I/O 是一种 没有阻塞地 读写资料的方法。通常,在程式码进行 read() 呼叫时,程式码会阻塞直至有可供读取的资料。同样, write() 呼叫将会阻塞直至资料能够写入。

另一方面,异步 I/O 呼叫不会阻塞。相反,您将注册对特定 I/O 事件的兴趣 ― 可读的资料的到达、新的套接字连线,等等,而在发生这样的事件时,系统将会告诉您。

异步 I/O 的一个优势在于,它允许您同时根据大量的输入和输出执行 I/O。同步程式常常要求助于轮询,或者建立许许多多的执行绪以处理大量的连线。使用异步 I/O,您可以监听任何数量的通道上的事件,不用轮询,也不用额外的执行绪。

我们将通过研究一个名为 MultiPortEcho.java(请从文末附件下载之) 的例子程式来检视异步 I/O 的实际应用。这个程式就像传统的 echo server,它接受网络连线并向它们回响它们可能传送的资料。不过它有一个附加的特性,就是它能同时监听多个埠,并处理来自所有这些埠的连线。并且它只在单个执行绪中完成所有这些工作。

11.3 Selectors

本节的阐述对应于 MultiPortEcho 的源代码中的 go() 方法的实现,因此应该看一下源代码,以便对所发生的事情有个更全面的了解。

异步 I/O 中的核心物件名为 Selector。Selector 就是您注册对各种 I/O 事件的兴趣的地方,而且当那些事件发生时,就是这个物件告诉您所发生的事件。

所以,我们需要做的第一件事就是建立一个 Selector:

Selector selector = Selector.open();

然后,我们将对不同的通道物件呼叫 register() 方法,以便注册我们对这些物件中发生的 I/O 事件的兴趣。register() 的第一个引数总是这个 Selector。

11.4 开启一个 ServerSocketChannel

为了接收连线,我们需要一个 ServerSocketChannel。事实上,我们要监听的每一个埠都需要有一个 ServerSocketChannel 。

对于每一个埠,我们开启一个 ServerSocketChannel,如下所示:

ServerSocketChannel ssc = ServerSocketChannel.open();

ssc.configureBlocking( false);

ServerSocket ss = ssc.socket();

InetSocketAddress address = newInetSocketAddress( ports[ii] );

ss.bind( address );

第一行建立一个新的 ServerSocketChannel ,最后三行将它系结到给定的埠。第二行将 ServerSocketChannel 设定为 非阻塞的 。我们必须对每一个要使用的套接字通道呼叫这个方法,否则异步 I/O 就不能工作。

11.5 选择键

下一步是将新开启的 ServerSocketChannels 注册到 Selector上。为此我们使用 ServerSocketChannel.register() 方法,如下所示:

SelectionKey key = ssc.register( selector, SelectionKey.OP_ACCEPT );

register() 的第一个引数总是这个 Selector。第二个引数是 OP_ACCEPT,这里它指定我们想要监听 accept 事件,也就是在新的连线建立时所发生的事件。这是适用于 ServerSocketChannel 的唯一事件型别。

请注意对 register() 的呼叫的返回值。 SelectionKey 代表这个通道在此 Selector 上的这个注册。当某个 Selector 通知您某个传入事件时,它是通过提供对应于该事件的 SelectionKey 来进行的。SelectionKey 还可以用于取消通道的注册。

11.6 内部循环

现在已经注册了我们对一些 I/O 事件的兴趣,下面将进入主循环。使用 Selectors 的几乎每个程式都像下面这样使用内部循环:

intnum = selector.select();

Set selectedKeys = selector.selectedKeys();

Iterator it = selectedKeys.iterator();

while(it.hasNext()) {

SelectionKey key = (SelectionKey)it.next();

// ... deal with I/O event ...

}

首先,我们呼叫 Selector 的 select() 方法。这个方法会阻塞,直到至少有一个已注册的事件发生。当一个或者更多的事件发生时, select() 方法将返回所发生的事件的数量。

接下来,我们呼叫 Selector 的 selectedKeys() 方法,它返回发生了事件的 SelectionKey 物件的一个 集合 。

我们通过迭代 SelectionKeys 并依次处理每个 SelectionKey 来处理事件。对于每一个 SelectionKey,您必须确定发生的是什么 I/O 事件,以及这个事件影响哪些 I/O 物件。

11.7 监听新连线

程式执行到这里,我们仅注册了 ServerSocketChannel,并且仅注册它们“接收”事件。为确认这一点,我们对 SelectionKey 呼叫 readyOps() 方法,并检查发生了什么型别的事件:

if((key.readyOps() & SelectionKey.OP_ACCEPT)

== SelectionKey.OP_ACCEPT) {

// Accept the new connection

// ...

}

可以肯定地说, readOps() 方法告诉我们该事件是新的连线。

11.8 接受新的连线

因为我们知道这个服务器套接字上有一个传入连线在等待,所以可以安全地接受它;也就是说,不用担心 accept() 操作会阻塞:

ServerSocketChannel ssc = (ServerSocketChannel)key.channel();

SocketChannel sc = ssc.accept();

下一步是将新连线的 SocketChannel 配置为非阻塞的。而且由于接受这个连线的目的是为了读取来自套接字的资料,所以我们还必须将 SocketChannel 注册到 Selector上,如下所示:

sc.configureBlocking( false);

SelectionKey newKey = sc.register( selector, SelectionKey.OP_READ );

注意我们使用 register() 的 OP_READ 引数,将 SocketChannel 注册用于 读取 而不是 接受 新连线。

11.9 删除处理过的 SelectionKey

在处理 SelectionKey 之后,我们几乎可以返回主循环了。但是我们必须首先将处理过的 SelectionKey 从选定的键集合中删除。如果我们没有删除处理过的键,那么它仍然会在主集合中以一个启用的键出现,这会导致我们尝试再次处理它。我们呼叫迭代器的 remove() 方法来删除处理过的 SelectionKey:

it.remove();

现在我们可以返回主循环并接受从一个套接字中传入的资料(或者一个传入的 I/O 事件)了。

11.10 传入的 I/O

当来自一个套接字的资料到达时,它会触发一个 I/O 事件。这会导致在主循环中呼叫 Selector.select(),并返回一个或者多个 I/O 事件。这一次, SelectionKey 将被标记为 OP_READ 事件,如下所示:

} elseif((key.readyOps() & SelectionKey.OP_READ)

== SelectionKey.OP_READ) {

// Read the data

SocketChannel sc = (SocketChannel)key.channel();

// ...

}

与以前一样,我们取得发生 I/O 事件的通道并处理它。在本例中,由于这是一个 echo server,我们只希望从套接字中读取资料并马上将它传送回去。关于这个过程的细节,请参见 参考资料 中的源代码 (MultiPortEcho.java)。

11.11 回到主循环

每次返回主循环,我们都要呼叫 select 的 Selector()方法,并取得一组 SelectionKey。每个键代表一个 I/O 事件。我们处理事件,从选定的键集中删除 SelectionKey,然后返回主循环的顶部。

这个程式有点过于简单,因为它的目的只是展示异步 I/O 所涉及的技术。在现实的应用程序中,您需要通过将通道从 Selector 中删除来处理关闭的通道。而且您可能要使用多个执行绪。这个程式可以仅使用一个执行绪,因为它只是一个演示,但是在现实场景中,建立一个执行绪池来负责 I/O 事件处理中的耗时部分会更有意义。

12、字符集

12.1 概述

根据 Sun(现在是Oracle) 的文件,一个 Charset 是“十六位 Unicode 字元序列与字节序列之间的一个命名的对映”。实际上,一个 Charset 允许您以尽可能最具可移植性的方式读写字元序列。

Java 语言被定义为基于 Unicode。然而在实际上,许多人编写程式码时都假设一个字元在磁盘上或者在网络流中用一个字节表示。这种假设在许多情况下成立,但是并不是在所有情况下都成立,而且随着计算机变得对 Unicode 越来越友好,这个假设就日益变得不能成立了。

在本节中,我们将看一下如何使用 Charsets 以适合现代文字格式的方式处理文字资料。这里将使用的示例程式相当简单,不过,它触及了使用 Charset 的所有关键方面:为给定的字元编码建立 Charset,以及使用该 Charset 解码和编码文字资料。

12.2 编码/解码

要读和写文字,我们要分别使用 CharsetDecoder 和 CharsetEncoder。将它们称为 编码器 和 解码器 是有道理的。一个 字元 不再表示一个特定的位模式,而是表示字元系统中的一个实体。因此,由某个实际的位模式表示的字元必须以某种特定的 编码 来表示。

CharsetDecoder 用于将逐位表示的一串字元转换为具体的 char 值。同样,一个 CharsetEncoder 用于将字元转换回位。

在下一个小节中,我们将考察一个使用这些物件来读写资料的程式。

12.3 处理文字的正确方式

现在我们将分析这个例子程式 UseCharsets.java。这个程式非常简单 ― 它从一个档案中读取一些文字,并将该文字写入另一个档案。但是它把该资料当作文字资料,并使用 CharBuffer 来将该数句读入一个 CharsetDecoder 中。同样,它使用 CharsetEncoder 来写回该资料。

我们将假设字元以 ISO-8859-1(Latin1) 字符集(这是 ASCII 的标准扩充套件)的形式储存在磁盘上。尽管我们必须为使用 Unicode 做好准备,但是也必须认识到不同的档案是以不同的格式储存的,而 ASCII 无疑是非常普遍的一种格式。

事实上,每种 Java 实现都要求对以下字元编码提供完全的支援:

US-ASCII

ISO-8859-1

UTF-8

UTF-16BE

UTF-16LE

UTF-16

12.4 示例程式

在开启相应的档案、将输入资料读入名为 inputData 的 ByteBuffer 之后,我们的程式必须建立 ISO-8859-1 (Latin1) 字符集的一个例项:

Charset latin1 = Charset.forName( "ISO-8859-1");

然后,建立一个解码器(用于读取)和一个编码器 (用于写入):

CharsetDecoder decoder = latin1.newDecoder();

CharsetEncoder encoder = latin1.newEncoder();

为了将字节资料解码为一组字元,我们把 ByteBuffer 传递给 CharsetDecoder,结果得到一个 CharBuffer:

CharBuffer cb = decoder.decode( inputData );

如果想要处理字元,我们可以在程式的此处进行。但是我们只想无改变地将它写回,所以没有什么要做的。

要写回资料,我们必须使用 CharsetEncoder 将它转换回字节:

ByteBuffer outputData = encoder.encode( cb );

在转换完成之后,我们就可以将资料写到档案中了。

13、结束语

正如您所看到的, NIO 库有大量的特性。在一些新特性(例如档案锁定和字符集)提供新功能的同时,许多特性在优化方面也非常优秀。

在基础层次上,通道和缓冲区可以做的事情几乎都可以用原来的面向流的类来完成。但是通道和缓冲区允许以 快得多 的方式完成这些相同的旧操作 ― 事实上接近系统所允许的最大速度。

不过 NIO 最强大的长度之一在于,它提供了一种在 Java 语言中执行进行输入/输出的新的(也是迫切需要的)结构化方式。随诸如缓冲区、通道和异步 I/O 这些概念性(且可实现的)实体而来的,是我们重新思考 Java 程式中的 I/O过程的机会。这样,NIO 甚至为我们最熟悉的 I/O 过程也带来了新的活力,同时赋予我们通过和以前不同并且更好的方式执行它们的机会。

14、示例源代码附件下载

(请从连结 http://www.52im.net/thread-2640-1-1.html 的附件中下载)

附录:更多NIO相关文章

《Java新一代网络程式设计模型AIO原理及Linux系统AIO介绍》

《有关“为何选择Netty”的11个疑问及解答》

《开源NIO框架八卦——到底是先有MINA还是先有Netty?》

《选Netty还是Mina:深入研究与对比(一)》

《选Netty还是Mina:深入研究与对比(二)》

《NIO框架入门(一):服务端基于Netty4的UDP双向通讯Demo演示》

《NIO框架入门(二):服务端基于MINA2的UDP双向通讯Demo演示》

《NIO框架入门(三):iOS与MINA2、Netty4的跨平台UDP双向通讯实战》

《NIO框架入门(四):Android与MINA2、Netty4的跨平台UDP双向通讯实战》

《Netty 4.x学习(一):ByteBuf详解》

《Netty 4.x学习(二):Channel和Pipeline详解》

《Netty 4.x学习(三):执行绪模型详解》

《Apache Mina框架高阶篇(一):IoFilter详解》

《Apache Mina框架高阶篇(二):IoHandler详解》

《MINA2 执行绪原理总结(含简单测试例项)》

《Apache MINA2.0 开发指南(中文版)[附件下载]》

《MINA、Netty的源代码(线上阅读版)已整理发布》

《解决MINA资料传输中TCP的粘包、缺包问题(有源代码)》

《解决Mina中多个同类型Filter例项共存的问题》

《实践总结:Netty3.x升级Netty4.x遇到的那些坑(执行绪篇)》

《实践总结:Netty3.x VS Netty4.x的执行绪模型》

《详解Netty的安全性:原理介绍、程式码演示(上篇)》

《详解Netty的安全性:原理介绍、程式码演示(下篇)》

《详解Netty的优雅退出机制和原理》

《NIO框架详解:Netty的高效能之道》

《Twitter:如何使用Netty 4来减少JVM的GC开销(译文)》

《绝对干货:基于Netty实现海量接入的推送服务技术要点》

《Netty干货分享:京东京麦的生产级TCP闸道器技术实践总结》

《新手入门:目前为止最透彻的的Netty高效能原理和框架架构解析》

《写给初学者:Java高效能NIO框架Netty的学习方法和进阶策略》

《少啰嗦!一分钟带你读懂Java的NIO和经典IO的区别》

《史上最强Java NIO入门:担心从入门到放弃的,请读这篇!》

>> 更多同类文章 ……

(本文同步释出于:http://www.52im.net/thread-2640-1-1.html)

文章标签: 报价宝 降噪耳机价格 耳机价格 红米手机价格 华为手机价格 小米手机价格 电视机价格 笔记本电脑价格 笔记本价格 汽车价格 手机价格 笔记本电脑价格 小米手机价格 华为手机价格 红米手机价格